Worker threads — real parallelism for CPU-bound work, without a full process

A child process gives real parallelism but at the cost of a whole separate OS process; worker threads give the same real parallelism within the same process, at a much lower overhead — the right tool specifically when the problem is CPU-bound work, not I/O.

Advanced

3 min read

The problem worker threads solve: a heavy computation blocking everything else

const http = require("node:http");
 
function computeExpensiveHash(data) {
  // a genuinely CPU-intensive loop — recall the non-blocking-I/O lesson:
  // this BLOCKS the single thread for its entire duration, no matter what
  for (let i = 0; i < 100_000_000; i++) { /* real computation */ }
  return "result";
}
 
http.createServer((req, res) => {
  const result = computeExpensiveHash(req.body); // EVERY other request waits for this to finish
  res.end(result);
}).listen(3000);

A genuinely CPU-heavy computation running on Node's single JavaScript thread blocks everything else that thread would otherwise be doing — including handling every other incoming HTTP request — for its entire duration, exactly the CPU-bound limitation the non-blocking-I/O lesson flagged directly. Non-blocking I/O never helps here, since there's no idle waiting period to reclaim; the thread is genuinely busy the whole time.

Worker threads: a real, separate thread, within the same process

const { Worker } = require("node:worker_threads");
 
const worker = new Worker("./hash-worker.js", { workerData: { input: req.body } });
worker.on("message", (result) => { res.end(result); }); // the MAIN thread stays free the whole time
worker.on("error", (err) => { console.error(err); });
 
// hash-worker.js
const { workerData, parentPort } = require("node:worker_threads");
const result = computeExpensiveHash(workerData.input); // runs on a SEPARATE thread — doesn't block the main one
parentPort.postMessage(result);

worker_threads runs genuinely parallel JavaScript on a separate OS thread, but within the same Node process — lighter-weight than a full child process (no separate process creation, no separate V8 instance to spin up), while still providing real, actual parallelism for the CPU-bound work, freeing the main thread to keep handling other requests (I/O-bound work, exactly what it's already good at) while the worker grinds through the heavy computation.

No shared memory by default — communication is message-passing, just like fork

// Data passed via postMessage() is COPIED (structured clone), not shared —
// mutating it in the worker does NOT affect the original in the main thread,
// unless SharedArrayBuffer is deliberately used instead
worker.postMessage({ data: someArray }); // someArray is CLONED, not shared, by default

By default, data passed between the main thread and a worker via postMessage()/parentPort.postMessage() is copied (using the structured clone algorithm), not shared — the worker gets its own independent copy, and mutating it has no effect on the main thread's original. This sidesteps an entire category of real, hard-to-debug multi-threading bug (two threads racing to read/write the exact same memory) that genuinely shared-memory threading models in other languages have to actively guard against — SharedArrayBuffer exists for the rarer case where genuine shared memory is actually needed, but it's an explicit, deliberate opt-in, not the default.

When to actually reach for worker threads, vs when not to

Worker threads are worth the real overhead (thread creation, message-passing serialization cost) specifically for genuinely CPU-bound work — image processing, complex data transformation, cryptographic hashing of large inputs — not for I/O-bound work, which Node's single-threaded non-blocking model already handles well without any extra threads at all. Reaching for a worker thread to handle an ordinary database query or API call is solving a problem that doesn't exist; the thread would just sit there waiting for I/O exactly like the main thread already does efficiently, with none of worker threads' actual benefit and all of their real overhead.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does a genuinely CPU-heavy computation block an entire Node server, even one that's otherwise well-optimized?

2. How does a worker thread differ from a full child process for handling CPU-bound work?

3. Is data passed between the main thread and a worker via postMessage() shared, or copied?