Worker Threads
Run CPU-intensive JavaScript in parallel using Node.js Worker Threads and shared memory
Overview
Key Points
- β’Worker threads run in the same process but on separate OS threads β lower startup cost than child processes
- β’Use workerData to pass initial data to a worker at creation time (read-only after creation)
- β’parentPort.postMessage() and worker.postMessage() exchange data via the Structured Clone Algorithm
- β’ArrayBuffers can be transferred (moved, not copied) for zero-copy communication with the transfer list
- β’SharedArrayBuffer enables shared memory between threads; use Atomics for safe concurrent access
- β’Worker threads are ideal for CPU work; child processes are better for running other executables or isolating crashes
Examples
Basic worker thread β offloading CPU work from the main thread
// main.js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const path = require('path');
if (isMainThread) {
// --- Main thread ---
const worker = new Worker(__filename, {
workerData: { start: 1, end: 50_000_000 },
});
worker.on('message', (result) => {
console.log('Sum:', result); // result from worker
});
worker.on('error', (err) => console.error('Worker error:', err));
worker.on('exit', (code) => console.log('Worker exited with code', code));
console.log('Main thread continues while worker computes...');
} else {
// --- Worker thread ---
const { start, end } = workerData;
let sum = 0;
for (let i = start; i <= end; i++) sum += i;
parentPort.postMessage(sum);
}isMainThread lets you put worker code in the same file. In production, separate workers into their own files for clarity.
Transferring an ArrayBuffer for zero-copy data passing
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Allocate a large buffer in the main thread
const buffer = new ArrayBuffer(1024 * 1024); // 1 MB
const view = new Uint8Array(buffer);
view.fill(42); // fill with test data
console.log('Buffer length before transfer:', buffer.byteLength); // 1048576
const worker = new Worker(__filename, {
workerData: { buffer },
transferList: [buffer], // TRANSFER ownership β no copy made
});
// buffer is now detached β byteLength is 0
console.log('Buffer length after transfer:', buffer.byteLength); // 0
worker.on('message', (result) => console.log('Sum in worker:', result));
} else {
const view = new Uint8Array(workerData.buffer);
const sum = view.reduce((acc, v) => acc + v, 0);
parentPort.postMessage(sum);
}After transferring, the original ArrayBuffer in the main thread becomes detached (byteLength === 0). The worker now owns the memory.
SharedArrayBuffer with Atomics for thread-safe counter
const { Worker, isMainThread, workerData } = require('worker_threads');
if (isMainThread) {
// Shared memory β both main and worker threads can read/write
const sharedBuffer = new SharedArrayBuffer(4); // 4 bytes = one Int32
const counter = new Int32Array(sharedBuffer);
const workers = Array.from({ length: 4 }, () =>
new Worker(__filename, { workerData: { sharedBuffer, increments: 250_000 } })
);
let done = 0;
for (const w of workers) {
w.on('exit', () => {
if (++done === workers.length) {
// All workers done β counter should be 4 * 250_000 = 1_000_000
console.log('Final counter:', Atomics.load(counter, 0));
}
});
}
} else {
const counter = new Int32Array(workerData.sharedBuffer);
for (let i = 0; i < workerData.increments; i++) {
Atomics.add(counter, 0, 1); // atomic increment β thread-safe
}
}Without Atomics.add(), concurrent counter++ would be a data race and produce incorrect results.
Common Mistakes
- Using regular objects in the transfer list β only ArrayBuffer and MessagePort can be transferred; other types throw a DataCloneError
- Writing to SharedArrayBuffer without Atomics β non-atomic reads and writes on shared memory produce data races and undefined behavior
- Creating thousands of workers β worker threads have overhead; pool them and reuse them rather than creating one per task
- Using Worker Threads for I/O-heavy work β the event loop already handles I/O non-blocking; worker threads only help with CPU-bound computation
Note
Build a worker pool instead of spawning a new Worker per task. Initialize N workers at startup, maintain a queue of pending tasks, and dispatch tasks to idle workers. This amortizes the startup cost over many tasks and bounds the number of OS threads created.
Exercises
Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4