Trust is earned, not given

A different perspective

2018-11-27 · Projects

Node.js, part 4: worker threads — real parallelism for CPU-bound work

Part 4from the Node.js series · 24 parts in all

Node's single thread is a feature until someone hands it a CPU-bound job. A tight loop blocks the event loop, so every other request waits — and async/await cannot help, because there is nothing to await. Worker threads (Node 10.5, stable later) are the answer: real OS threads, real cores, sharing nothing but a message channel.

The smallest useful worker

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // The worker loads this same file and takes the other branch.
  const worker = new Worker(__filename, { workerData: { n: 40 } });
  worker.on('message', (result) => console.log('fib(40) =', result));
  worker.on('error', (err) => console.error('worker failed', err));
  worker.on('exit', (code) => console.log('worker exited', code));
} else {
  const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
  parentPort.postMessage(fib(workerData.n));
}

The event loop is free while that runs. That is the whole point, and it is worth measuring once for yourself: the same fib(40) on the main thread makes a trivial HTTP server answer in tens of seconds, and in a worker it answers instantly.

What crosses the boundary

Messages are structured-cloned, not shared: no functions, no class instances with behaviour, no live references. Two exceptions matter in practice:

When a pool is the right shape, and when to look elsewhere

A worker per request is expensive — startup is milliseconds and each one costs memory. For a service, keep a pool sized to os.cpus().length and queue work to it. But check the premise first: if the job is I/O-bound, promises already overlap it and a worker adds overhead for nothing. Workers are for CPU: image resizing, parsing, compression, crypto, report generation. Next: the module system itself, and the seven-year migration to import.