Worker Threads vs Cluster
When to reach for threads (CPU work, shared memory) versus processes (scaling connections).
Node is single-threaded for your JavaScript, so a CPU-bound function blocks the event loop and every connection stalls. Two tools scale past one core, and they solve different problems.
worker_threads | cluster | |
|---|---|---|
| Unit | threads in one process | separate processes |
| Memory | can share via SharedArrayBuffer | fully isolated |
| Best for | CPU-bound work (parse, hash, image) | scaling I/O across cores |
| Comms | MessagePort, transferable buffers | IPC messages |
const { Worker } = require('node:worker_threads');
const w = new Worker('./cpu-task.js', { workerData: { n: 45 } });
w.on('message', (result) => console.log('fib =', result));
w.on('error', console.error);Rules of thumb
Offload CPU-bound work (crypto, compression, template rendering, parsing megabytes of JSON) to a pool of worker threads so the main loop stays free. Use cluster (or, in practice, an external process manager / container replicas) to run N copies of an I/O-bound server and spread connections across cores.
Example
// A minimal reusable worker pool for CPU-bound tasks. In real code use
// piscina; this shows the mechanics — dispatch, queueing, transfer.
const { Worker } = require('node:worker_threads');
const os = require('node:os');
class Pool {
constructor(file, size = os.availableParallelism()) {
this.idle = [];
this.queue = [];
for (let i = 0; i < size; i++) this.idle.push(new Worker(file));
}
run(data, transfer = []) {
return new Promise((resolve, reject) => {
const job = { data, transfer, resolve, reject };
const worker = this.idle.pop();
worker ? this._dispatch(worker, job) : this.queue.push(job);
});
}
_dispatch(worker, job) {
const done = (err, result) => {
worker.off('message', onMsg); worker.off('error', onErr);
err ? job.reject(err) : job.resolve(result);
const next = this.queue.shift();
next ? this._dispatch(worker, next) : this.idle.push(worker);
};
const onMsg = (r) => done(null, r);
const onErr = (e) => done(e);
worker.once('message', onMsg); worker.once('error', onErr);
worker.postMessage(job.data, job.transfer); // zero-copy for transfer list
}
}
const pool = new Pool('./hash-worker.js');
const results = await Promise.all(
Array.from({ length: 100 }, (_, i) => pool.run({ input: `payload-${i}` })),
);
console.log('hashed', results.length, 'payloads across all cores');When to use it
- A REST API offloads bcrypt password hashing (CPU-intensive) to a worker thread so the event loop stays free to handle other incoming requests.
- A report-generation service uses a pool of worker threads to compile PDF reports in parallel, cutting batch time proportionally to CPU core count.
- A game server delegates physics simulation to a worker thread so the main thread exclusively handles WebSocket message routing.
More examples
Spawn a worker thread
Spawns a worker thread to compute a CPU-intensive Fibonacci number and sends the result back via `postMessage`.
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./heavy.js', { workerData: { n: 40 } });
worker.on('message', result => console.log('Result:', result));
worker.on('error', err => console.error(err));
// heavy.js
const { workerData, parentPort } = require('worker_threads');
function fib(n) { return n <= 1 ? n : fib(n-1) + fib(n-2); }
parentPort.postMessage(fib(workerData.n));Share memory with SharedArrayBuffer
Uses `SharedArrayBuffer` and `Atomics` to let a worker increment a counter that the main thread reads without message-passing.
const { Worker, isMainThread, workerData } = require('worker_threads');
const shared = new SharedArrayBuffer(4);
const view = new Int32Array(shared);
if (isMainThread) {
new Worker(__filename, { workerData: { shared } });
setTimeout(() => console.log('Counter:', view[0]), 200);
} else {
const arr = new Int32Array(workerData.shared);
Atomics.add(arr, 0, 42); // atomic increment
}Worker thread pool pattern
Wraps each worker in a Promise so tasks can be dispatched in parallel and awaited with `Promise.all`.
const { Worker } = require('worker_threads');
function runTask(data) {
return new Promise((resolve, reject) => {
const w = new Worker('./task.js', { workerData: data });
w.once('message', resolve);
w.once('error', reject);
w.once('exit', code => {
if (code !== 0) reject(new Error(`Worker exited ${code}`));
});
});
}
const results = await Promise.all([1,2,3].map(n => runTask({ n })));
Discussion