The node:worker_threads module lets Node.js execute CPU-heavy JavaScript—such as image resizing, CSV parsing, PDF generation, or cryptography—on separate OS threads in parallel. Unlike network or file I/O (which the event loop already handles asynchronously), heavy CPU loops freeze the main thread unless moved into a worker thread.
If an HTTP API endpoint runs a 400-millisecond loop on the main thread, every other user waiting on that server stalls for 400 milliseconds. Moving that work to a worker keeps the main event loop free to answer incoming HTTP traffic instantly.
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
if (isMainThread) {
function runCpuTask(limit) {
return new Promise((resolve, reject) => {
const worker = new Worker(new URL(import.meta.url), { workerData: limit });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
});
});
}
runCpuTask(1_000_000).then((sum) => console.log('Calculated in worker:', sum));
} else {
let total = 0;
for (let i = 1; i <= workerData; i++) total += i;
parentPort.postMessage(total);
}
Messages sent through parentPort.postMessage() are cloned using the structured clone algorithm by default, which is clean and thread-safe. For large binary buffers (like raw image pixels), pass an ArrayBuffer in the transfer list to hand off ownership instantly with zero copying.
new Worker() for every incoming HTTP request: Creating OS threads and new V8 isolates takes tens of milliseconds and memory. Reuse a bounded worker pool (such as piscina) sized to os.availableParallelism().fetch() calls: Network I/O is already non-blocking on the main thread; putting fetch() inside a worker adds overhead without speeding up I/O.'error' and 'exit' events: If a worker throws an uncaught error, the Promise waiting on 'message' will hang forever unless 'error' and non-zero 'exit' reject it.No. Each worker gets its own isolated V8 JavaScript environment and event loop; threads communicate via postMessage() or explicit SharedArrayBuffer memory.
Cap CPU-bound worker pools at os.availableParallelism() (or cores - 1), because spawning 100 CPU workers on a 4-core machine causes thread-switching slowdowns.
.js file instead of checking isMainThread?Yes. Splitting worker-task.js into its own file and calling new Worker(new URL('./worker-task.js', import.meta.url)) is cleaner in real applications.
Sign in to track your learning journey, earn industry-recognized certificates, and join our elite developer community.
Quick Access With
Experiment with the code from this lesson in our interactive playground.