Suggest an editImprove this articleRefine the answer for “What are worker threads in Node.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Worker threads** in Node.js run JavaScript in parallel threads inside the same process, built for CPU-intensive tasks that would block the event loop. ```js const { Worker } = require('worker_threads'); const worker = new Worker('./compute.js', { workerData: { n: 1e8 } }); worker.on('message', result => console.log(result)); console.log('Main thread stays free'); // runs immediately ``` **Key point:** unlike the Cluster module which spawns separate processes, worker threads share memory via `SharedArrayBuffer` and communicate through `MessagePort` without process overhead.Shown above the full answer for quick recall.Answer (EN)Image**Worker threads** in Node.js run JavaScript in parallel threads inside the same process, designed for CPU-intensive tasks that would otherwise block the [event loop](/questions/nodejs-event-loop). ## Theory ### TL;DR - Worker threads live in the same process but execute on separate OS threads - They share memory via `SharedArrayBuffer`, unlike the [Cluster module](/questions/nodejs-cluster) which spawns separate processes - Use them for CPU-bound work: image processing, cryptography, heavy parsing - For I/O tasks (HTTP requests, file reads), the event loop handles them without threads - Experimental in Node.js 10, stable since Node.js 12 ### Quick example ```js // worker.js const { workerData, parentPort } = require('worker_threads'); let result = 0; for (let i = 0; i < workerData.n; i++) result += Math.sqrt(i); parentPort.postMessage(result); // send result to main thread ``` ```js // main.js const { Worker } = require('worker_threads'); const worker = new Worker('./worker.js', { workerData: { n: 1e8 } }); worker.on('message', result => console.log('Result:', result)); console.log('Main thread keeps running'); // prints immediately ``` The main thread does not wait. While the worker runs `Math.sqrt` 100 million times, the main thread stays free to handle requests or other events. ### Why the event loop is not enough for CPU work Node.js runs JavaScript on a single thread. That is intentional. The event loop works well for I/O because waiting happens in libuv's thread pool or at the OS level. The JS thread just processes callbacks when work is done. CPU work is different. A 4-second hash computation in the main thread holds up every incoming request for those 4 seconds. Worker threads give that computation its own thread, so the main thread stays clear. ### How worker threads work internally Each `Worker` instance creates a new V8 isolate (a separate JavaScript engine context) with its own event loop. The worker runs completely independently. It has its own module system, globals, and memory heap. Communication between threads goes through `MessagePort`. Data is cloned via the structured clone algorithm by default. Passing a 10MB object copies 10MB. That cloning cost adds up under load. To skip copying, you can transfer ownership of objects with `{ transfer: [...] }`, or use `SharedArrayBuffer` to give both threads access to the same physical memory. ### Shared memory with SharedArrayBuffer ```js const { Worker } = require('worker_threads'); const buffer = new SharedArrayBuffer(4); // 4 bytes, shared between threads const view = new Int32Array(buffer); view[0] = 0; const worker = new Worker(` const { workerData } = require('worker_threads'); const arr = new Int32Array(workerData.buffer); Atomics.store(arr, 0, 42); // thread-safe write `, { eval: true, workerData: { buffer } }); worker.on('exit', () => { console.log(view[0]); // 42 }); ``` Notice `Atomics.store`. Without it, two threads writing to the same memory location at the same time produce race conditions. `Atomics` provides thread-safe operations on `SharedArrayBuffer`. ### Worker pool pattern Creating a new worker per request is expensive. V8 needs to initialize a new isolate each time, which takes 50-100ms. In production, you pre-create a pool of workers and reuse them. After debugging a production incident where missing error handlers caused worker failures to silently hang Promises, I now treat pool implementation as something that needs error handling built in from the start. ```js const { Worker } = require('worker_threads'); const os = require('os'); class WorkerPool { constructor(workerFile, size = os.cpus().length) { this.workers = Array.from({ length: size }, () => ({ worker: new Worker(workerFile), busy: false })); this.queue = []; } run(data) { return new Promise((resolve, reject) => { const free = this.workers.find(w => !w.busy); if (free) { this._execute(free, data, resolve, reject); } else { this.queue.push({ data, resolve, reject }); } }); } _execute(entry, data, resolve, reject) { entry.busy = true; entry.worker.postMessage(data); entry.worker.once('message', result => { entry.busy = false; resolve(result); if (this.queue.length > 0) { const next = this.queue.shift(); this._execute(entry, next.data, next.resolve, next.reject); } }); entry.worker.once('error', reject); } } ``` Pool size defaults to `os.cpus().length` because that is how many threads the CPU can run in parallel. More workers than CPU cores adds context switching overhead with no performance benefit. ### Worker threads vs Cluster | | Worker Threads | Cluster | |---|---|---| | Process model | Same process | Separate processes | | Memory | Shared via `SharedArrayBuffer` | Isolated | | Communication | `MessagePort` (fast, structured clone) | IPC (slower) | | Crash isolation | Low (worker error can affect the process) | High (process crash stays contained) | | Best for | CPU-bound computation | Scaling HTTP servers across CPU cores | Cluster is the right tool when multiple workers need to handle incoming connections. Worker threads are the right tool when one task needs to run heavy computation without blocking everything else. ### Common mistakes **1. Using worker threads for I/O** ```js // Wrong - spawns a whole thread just to read a file const worker = new Worker('./readFile.js'); // Right - the event loop handles this natively const data = await fs.promises.readFile('./file.txt'); ``` I/O is async by design. A worker here adds overhead with no gain. **2. Skipping Atomics on SharedArrayBuffer** ```js // Race condition - reads and writes can interleave shared[0]++; // Thread-safe Atomics.add(shared, 0, 1); ``` Two threads incrementing the same value without `Atomics` will lose updates. **3. Creating a new worker per request** ```js // Expensive - new V8 isolate on every request app.get('/compute', (req, res) => { const w = new Worker('./compute.js'); w.on('message', result => res.json({ result })); }); ``` Use a pool. Under load, 100ms startup time per request is a serious problem. **4. Copying large buffers through postMessage** ```js // Copies 50MB on every call worker.postMessage({ data: hugeBuffer }); // Transfer ownership instead (zero-copy) worker.postMessage({ data: hugeBuffer }, [hugeBuffer]); // hugeBuffer is now detached in the main thread ``` **5. Missing error handling** ```js // If the worker throws, this Promise hangs forever const worker = new Worker('./task.js'); worker.on('message', resolve); // Always add both: worker.on('error', reject); worker.on('exit', code => { if (code !== 0) reject(new Error(`Worker stopped with code ${code}`)); }); ``` ### Where worker threads appear in real codebases - `sharp` (image processing) uses workers for resize and format conversion - `bcrypt` hashing in auth services moves to workers so password checks do not delay API responses - Webpack and esbuild use worker pools for parallel module transformation - `piscina` is a widely-used worker thread pool library that replaces hand-rolled pool implementations - Jest runs test files in worker threads for parallel test execution ### Follow-up questions **Q:** What is the difference between `workerData` and `postMessage`? **A:** `workerData` is read-only data passed at worker creation time. `postMessage` sends messages in both directions after the worker is running. Use `workerData` for initial config, `postMessage` for ongoing communication. **Q:** Can worker threads share the same `require` cache? **A:** No. Each worker has its own V8 isolate and module cache. `require('lodash')` in a worker loads lodash separately from the main thread. This is part of why worker startup has a measurable cost. **Q:** What happens if a worker throws an unhandled error? **A:** The worker emits an `'error'` event and terminates. If no listener is attached, the error propagates to the main thread as an uncaught exception and crashes the process. **Q:** How does `SharedArrayBuffer` differ from `postMessage` for data transfer? **A:** `postMessage` copies data via structured clone. A 5MB buffer costs 5MB of extra memory and time to clone. `SharedArrayBuffer` gives both threads access to the same physical memory with no copying, but requires `Atomics` for safe concurrent access. **Q:** When should you use worker threads versus a child process? **A:** Worker threads when the task is compute-heavy and you want low-overhead communication or shared memory. Child processes when you need full isolation (a crash in `child_process` does not affect the parent) or when running an external executable. ## Examples ### Basic CPU-bound task off the main thread ```js // hash-worker.js const { workerData, parentPort } = require('worker_threads'); const crypto = require('crypto'); // Synchronous hashing - would block the event loop on the main thread const hash = crypto .createHash('sha256') .update(workerData.payload.repeat(10000)) .digest('hex'); parentPort.postMessage(hash); ``` ```js // server.js const { Worker } = require('worker_threads'); const http = require('http'); function hashInWorker(payload) { return new Promise((resolve, reject) => { const worker = new Worker('./hash-worker.js', { workerData: { payload } }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', code => { if (code !== 0) reject(new Error(`Worker exited: ${code}`)); }); }); } http.createServer(async (req, res) => { const hash = await hashInWorker('user-password'); res.end(hash); }).listen(3000); ``` The HTTP server handles every request without blocking. Each hash runs in its own thread. The main thread stays free. ### Thread-safe counter with SharedArrayBuffer and Atomics ```js // counter-worker.js const { workerData } = require('worker_threads'); const counter = new Int32Array(workerData.buffer); for (let i = 0; i < 1000; i++) { Atomics.add(counter, 0, 1); // thread-safe increment } ``` ```js // main.js const { Worker } = require('worker_threads'); const buffer = new SharedArrayBuffer(4); const counter = new Int32Array(buffer); const workers = Array.from({ length: 4 }, () => new Worker('./counter-worker.js', { workerData: { buffer } }) ); Promise.all( workers.map(w => new Promise(resolve => w.on('exit', resolve))) ).then(() => { console.log(counter[0]); // exactly 4000 }); ``` Four workers each increment the counter 1000 times. `Atomics.add` makes sure no update is lost to a race condition. ### Worker pool for image resize requests ```js // image-pool.js const { Worker } = require('worker_threads'); const os = require('os'); const path = require('path'); class ImagePool { constructor() { this.pool = Array.from({ length: os.cpus().length }, () => ({ worker: new Worker(path.join(__dirname, 'image-worker.js')), busy: false })); this.pending = []; } resize(imagePath, width, height) { return new Promise((resolve, reject) => { const free = this.pool.find(p => !p.busy); if (free) { this._dispatch(free, { imagePath, width, height }, resolve, reject); } else { this.pending.push({ data: { imagePath, width, height }, resolve, reject }); } }); } _dispatch(entry, data, resolve, reject) { entry.busy = true; entry.worker.postMessage(data); entry.worker.once('message', result => { entry.busy = false; resolve(result); if (this.pending.length > 0) { const next = this.pending.shift(); this._dispatch(entry, next.data, next.resolve, next.reject); } }); entry.worker.once('error', err => { entry.busy = false; reject(err); }); } } module.exports = new ImagePool(); ``` One pool instance shared across the whole application. Resize requests queue up and get processed as workers become free, without spawning new processes on each call.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.