worker_threads
Worker threads let Node offload CPU-bound work to a separate V8 isolate without blocking the event loop. Use them for image encoding, hashing, ZIP compression, parsing, or any pure-CPU task longer than a few milliseconds. They are not the right tool for I/O — async I/O on the main thread already scales further.
Run a CPU-bound task in a worker pool
EXAMPLE
// worker.js — runs in its own thread, communicates by messages
const { parentPort } = require('node:worker_threads');
const crypto = require('node:crypto');
parentPort.on('message', (job) => {
// Pretend-expensive CPU work: bcrypt-style salted hash
const salt = crypto.randomBytes(16);
let hash = Buffer.from(job.password, 'utf8');
for (let i = 0; i < 100_000; i++) {
hash = crypto.createHash('sha256').update(hash).update(salt).digest();
}
parentPort.postMessage({ id: job.id, hash: hash.toString('hex') });
});
// pool.js — a small fixed-size worker pool
const { Worker } = require('node:worker_threads');
const os = require('node:os');
class WorkerPool {
constructor(file, size = os.availableParallelism()) {
this.workers = Array.from({ length: size }, () => ({
w: new Worker(file),
busy: false,
}));
this.queue = [];
}
run(job) {
return new Promise((resolve, reject) => {
const task = { job, resolve, reject };
const idle = this.workers.find((x) => !x.busy);
if (idle) this._dispatch(idle, task);
else this.queue.push(task);
});
}
_dispatch(slot, task) {
slot.busy = true;
const onMsg = (msg) => {
slot.w.off('message', onMsg);
slot.w.off('error', onErr);
slot.busy = false;
task.resolve(msg);
const next = this.queue.shift();
if (next) this._dispatch(slot, next);
};
const onErr = (e) => { slot.w.off('message', onMsg); slot.busy = false; task.reject(e); };
slot.w.once('message', onMsg);
slot.w.once('error', onErr);
slot.w.postMessage(task.job);
}
async close() {
await Promise.all(this.workers.map((x) => x.w.terminate()));
}
}
// main.js
const pool = new WorkerPool(require.resolve('./worker.js'), 4);
const results = await Promise.all(
Array.from({ length: 20 }, (_, i) => pool.run({ id: i, password: 'hunter2' }))
);
console.log(results[0]);
await pool.close();
Why it matters
A pool of size N = availableParallelism() is the sweet spot — more workers than cores just adds context-switch overhead. Measure: if your task is shorter than ~1ms, the postMessage round-trip will eat the gain and a plain async call is faster.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// worker.js
import { parentPort } from 'node:worker_threads';
parentPort.on('message', n => parentPort.postMessage(n * 2));
Try it Yourself »
Discussion
Loading…