Worker Threads vs Cluster

When to reach for threads (CPU work, shared memory) versus processes (scaling connections).

Node is single-threaded for your JavaScript, so a CPU-bound function blocks the event loop and every connection stalls. Two tools scale past one core, and they solve different problems.

worker_threadscluster
Unitthreads in one processseparate processes
Memorycan share via SharedArrayBufferfully isolated
Best forCPU-bound work (parse, hash, image)scaling I/O across cores
CommsMessagePort, transferable buffersIPC messages
const { Worker } = require('node:worker_threads');

const w = new Worker('./cpu-task.js', { workerData: { n: 45 } });
w.on('message', (result) => console.log('fib =', result));
w.on('error', console.error);

Rules of thumb

Offload CPU-bound work (crypto, compression, template rendering, parsing megabytes of JSON) to a pool of worker threads so the main loop stays free. Use cluster (or, in practice, an external process manager / container replicas) to run N copies of an I/O-bound server and spread connections across cores.

Example

Example · javascript
// A minimal reusable worker pool for CPU-bound tasks. In real code use
// piscina; this shows the mechanics — dispatch, queueing, transfer.
const { Worker } = require('node:worker_threads');
const os = require('node:os');

class Pool {
  constructor(file, size = os.availableParallelism()) {
    this.idle = [];
    this.queue = [];
    for (let i = 0; i < size; i++) this.idle.push(new Worker(file));
  }
  run(data, transfer = []) {
    return new Promise((resolve, reject) => {
      const job = { data, transfer, resolve, reject };
      const worker = this.idle.pop();
      worker ? this._dispatch(worker, job) : this.queue.push(job);
    });
  }
  _dispatch(worker, job) {
    const done = (err, result) => {
      worker.off('message', onMsg); worker.off('error', onErr);
      err ? job.reject(err) : job.resolve(result);
      const next = this.queue.shift();
      next ? this._dispatch(worker, next) : this.idle.push(worker);
    };
    const onMsg = (r) => done(null, r);
    const onErr = (e) => done(e);
    worker.once('message', onMsg); worker.once('error', onErr);
    worker.postMessage(job.data, job.transfer); // zero-copy for transfer list
  }
}

const pool = new Pool('./hash-worker.js');
const results = await Promise.all(
  Array.from({ length: 100 }, (_, i) => pool.run({ input: `payload-${i}` })),
);
console.log('hashed', results.length, 'payloads across all cores');

When to use it

  • A REST API offloads bcrypt password hashing (CPU-intensive) to a worker thread so the event loop stays free to handle other incoming requests.
  • A report-generation service uses a pool of worker threads to compile PDF reports in parallel, cutting batch time proportionally to CPU core count.
  • A game server delegates physics simulation to a worker thread so the main thread exclusively handles WebSocket message routing.

More examples

Spawn a worker thread

Spawns a worker thread to compute a CPU-intensive Fibonacci number and sends the result back via `postMessage`.

Example · js
// main.js
const { Worker } = require('worker_threads');

const worker = new Worker('./heavy.js', { workerData: { n: 40 } });
worker.on('message', result => console.log('Result:', result));
worker.on('error',   err    => console.error(err));

// heavy.js
const { workerData, parentPort } = require('worker_threads');
function fib(n) { return n <= 1 ? n : fib(n-1) + fib(n-2); }
parentPort.postMessage(fib(workerData.n));

Share memory with SharedArrayBuffer

Uses `SharedArrayBuffer` and `Atomics` to let a worker increment a counter that the main thread reads without message-passing.

Example · js
const { Worker, isMainThread, workerData } = require('worker_threads');
const shared = new SharedArrayBuffer(4);
const view   = new Int32Array(shared);

if (isMainThread) {
  new Worker(__filename, { workerData: { shared } });
  setTimeout(() => console.log('Counter:', view[0]), 200);
} else {
  const arr = new Int32Array(workerData.shared);
  Atomics.add(arr, 0, 42); // atomic increment
}

Worker thread pool pattern

Wraps each worker in a Promise so tasks can be dispatched in parallel and awaited with `Promise.all`.

Example · js
const { Worker } = require('worker_threads');

function runTask(data) {
  return new Promise((resolve, reject) => {
    const w = new Worker('./task.js', { workerData: data });
    w.once('message', resolve);
    w.once('error',   reject);
    w.once('exit', code => {
      if (code !== 0) reject(new Error(`Worker exited ${code}`));
    });
  });
}

const results = await Promise.all([1,2,3].map(n => runTask({ n })));

Discussion

  • Be the first to comment on this lesson.