Back to Node.js tutorials
Advanced16 min read

Worker Threads

Offload CPU-intensive work from the event loop using worker_threads and transferable buffers.

When to Use Workers

Node event loop stalls when CPU-bound JavaScript runs synchronously—image processing, compression, cryptography, large JSON parsing. worker_threads run JavaScript in parallel threads with isolated V8 isolates.

Workers are not free: startup cost and message serialization overhead matter for tiny tasks. Batch work or pool workers for throughput.

For I/O-bound tasks, async APIs and more processes often beat threads.

  • Profile before parallelizing; event loop may not be the bottleneck
  • Prefer pool size near physical CPU cores
  • Combine workers with cluster for mixed I/O and CPU servers

Creating Workers

Import worker_threads and create Worker with a file path or eval script. Main thread listens for message events; worker posts results with parentPort.postMessage.

Pass configuration via workerData at construction. Handle error and exit events to restart crashed workers in pools.

Use Atomics and SharedArrayBuffer only when shared memory is truly required—most apps message plain data objects.

  • Structure worker files with isMainThread guard
  • Terminate workers on timeout for runaway jobs
  • Log worker stderr separately in production
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';

if (isMainThread) {
  const worker = new Worker(new URL(import.meta.url));
  worker.on('message', console.log);
  worker.postMessage({ job: workerData });
} else {
  parentPort.on('message', msg => parentPort.postMessage(processJob(msg)));
}

Transferable Objects and Pools

ArrayBuffer can transfer ownership to workers without copying using postMessage second argument list. This speeds large binary processing.

Worker pools queue jobs and reuse workers to amortize startup. Libraries like piscina wrap best practices.

Measure end-to-end latency including serialization. Sometimes splitting work across fewer larger chunks wins.

  • Avoid sending huge cloned objects every message
  • Cap queue depth to prevent memory spikes under load
  • Expose pool stats in health endpoints during tuning
import Piscina from 'piscina';
const pool = new Piscina({ filename: new URL('./worker.js', import.meta.url) });
const result = await pool.run({ buffer });

Get In Touch


Ready to discuss your next project? Drop me a message.