Using Node.js Worker Threads to Offload CPU‑Intensive Tasks
Learn how to use Node.js worker threads to run CPU‑intensive tasks without blocking the event loop. Step‑by‑step guide, example, checks, and recovery options.
03 May 2026, 20:09 UTC

Desired Outcome
Make a Node.js application that performs heavy CPU work—such as calculating a large prime—without blocking the event loop, keeping I/O responsive, and cleaning up worker resources cleanly.
Prerequisites
- Node.js 12.x or newer (v18 LTS is recommended). Verify with
node -v. - Basic understanding of JavaScript modules and asynchronous programming.
- No external dependencies are required for the core example.
Focused Procedure
Create the Worker Script
Place the following in
prime-worker.js. The worker receives a number and returns the next prime greater than that number.const { parentPort } = require('worker_threads'); function isPrime(n) { if (n <= 1) return false; if (n <= 3) return true; if (n % 2 === 0 || n % 3 === 0) return false; for (let i = 5; i * i <= n; i += 6) { if (n % i === 0 || n % (i + 2) === 0) return false; } return true; } function nextPrime(start) { let n = start + 1; while (!isPrime(n)) n++; return n; } parentPort.on('message', (msg) => { try { const result = nextPrime(msg.number); parentPort.postMessage({ result }); } catch (err) { parentPort.postMessage({ error: err.message }); } });Create the Main Thread Script
Use
worker_threadsto spawn the worker, send data, and handle responses. Save asmain.js.const { Worker } = require('worker_threads'); function runWorker(number) { return new Promise((resolve, reject) => { const worker = new Worker('./prime-worker.js'); // Log when worker starts console.log(`Worker started for number ${number}`); worker.on('message', (msg) => { if (msg.error) { reject(new Error(msg.error)); } else { resolve(msg.result); } }); worker.on('error', (err) => reject(err)); worker.on('exit', (code) => { if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } }); // Send the number to the worker worker.postMessage({ number }); }); } (async () => { const start = Date.now(); console.log('Main thread is free to handle I/O…'); try { const prime = await runWorker(1e7); // large number for CPU load console.log(`Next prime after 10,000,000 is ${prime}`); } catch (err) { console.error('Worker error:', err); } console.log(`Total elapsed: ${Date.now() - start} ms`); })();Run and Verify Responsiveness
Execute:
node main.jsWhile the worker calculates the next prime, the main thread can still process other events. If you add a quick timer after launching the worker, you’ll see it fires before the CPU task completes:
setTimeout(() => console.log('Timer fired after 1s'), 1000);Graceful Termination (Optional)
If you need to cancel a long‑running worker, call
worker.terminate()and handle theexitevent. Example:const worker = new Worker('./prime-worker.js'); // ... setTimeout(() => { worker.terminate().then(() => console.log('Worker terminated')); }, 5000); // cancel after 5s
Expected Checks
- Verify Node version:
node -vshould output v12+. - After running
node main.js, the console should show:- "Worker started for number 10000000"
- "Main thread is free to handle I/O…"
- "Timer fired after 1s" (if timer added)
- "Next prime after 10,000,000 is 10000019" (or similar)
- "Total elapsed: X ms"
- No Node processes should remain after the script exits. Use
ps -ef | grep nodeto confirm. - If you intentionally terminate the worker early, the main thread should log "Worker terminated" and exit without hanging.
Recovery Options
- Worker Crash: The
errorandexitlisteners catch exceptions. Log the error, optionally restart the worker by callingrunWorker()again. - Unresponsive Worker: If the worker takes unusually long, consider setting a timeout in the main thread. If exceeded, call
worker.terminate()and handle cleanup. - Memory Leak: Workers share memory via
SharedArrayBufferor transferable objects. Ensure you free any allocated buffers after use by dropping references; Node will GC them.
Limitations and Caveats
- Worker threads cannot directly use timers or most Node I/O APIs that depend on the event loop. Keep such operations in the main thread.
- In older Node.js releases,
SharedArrayBufferrequires the--experimental-modulesflag. From v14 onward, this is stable. - When running under a Content Security Policy (e.g., in Electron), SharedArrayBuffer may be disabled unless the CSP allows it.
- Each worker consumes a separate thread; spawning many workers can exhaust system threads. Use a pool or limit concurrency.
Practical Check for Responsiveness
Insert a short timer in the main thread before launching the worker. If the timer logs before the worker completes, the event loop is not blocked.
console.log('Before worker');
setTimeout(() => console.log('Timer after 500ms'), 500);
runWorker(1e7).then(console.log);
Conclusion
Node.js worker threads give you a native, low‑overhead way to offload CPU‑bound work. By following the pattern above—creating a dedicated worker script, communicating via postMessage, and handling lifecycle events—you can keep your application responsive while still performing heavy calculations. Remember to monitor worker health and clean up resources to avoid leaks or orphaned threads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.