Skip to content

Node.js Worker Threads

Async code doesn't help when work is genuinely CPU-bound, calculations that take real time on the CPU itself. This lesson covers Worker Threads, Node's solution for running heavy computation without blocking the main thread.

Why Worker Threads Exist

Async/await and promises solve I/O-bound waiting, they don't make CPU-bound work (image processing, complex calculations, large data transformations) run any faster or avoid blocking the main thread, because that work genuinely needs the CPU the whole time, there's no "waiting" to yield during.

The worker_threads module lets you run JavaScript on a genuinely separate thread, with its own V8 instance and event loop, communicating with the main thread via message passing. This keeps the main thread free to handle other requests while the worker crunches through CPU-heavy work.

// worker.js
import { parentPort, workerData } from 'node:worker_threads';

function fibonacci(n) {
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

parentPort.postMessage(fibonacci(workerData.n));

// main.js
import { Worker } from 'node:worker_threads';

const worker = new Worker('./worker.js', { workerData: { n: 40 } });

worker.on('message', (result) => {
  console.log('Fibonacci result:', result);
});

The heavy fibonacci(40) computation runs entirely on the worker thread; the main thread's event loop stays completely free the whole time.

Core Worker Threads API

new Worker('./worker.js', { workerData })
worker.on('message', (data) => { ... })
worker.on('error', (err) => { ... })
worker.postMessage(data)   // send data TO the worker
parentPort.postMessage(data) // send data FROM the worker
  • workerData passes initial data into a worker when it's created.
  • worker.postMessage()/parentPort.postMessage() send messages between threads after creation.
  • Workers have their own memory space, data is copied (or transferred) between threads, not shared by default.
  • Always attach an 'error' listener to a worker, unhandled worker errors can crash the whole process.

Worker Threads Cheatsheet

When to reach for Worker Threads versus other approaches.

Use Case Best Fit
Waiting on a database/API Regular async/await, no worker needed
Heavy CPU computation Worker Threads
Image/video processing Worker Threads (or a dedicated service)
Running multiple server instances Clustering (covered next)
Shell commands/external programs Child Process (covered next)

Reusing Workers With a Pool

Creating a new worker thread has real startup overhead. For applications that repeatedly need CPU-bound work done, a pool of pre-started, reusable workers (similar in spirit to a database connection pool) avoids paying that startup cost on every single task.

  • Libraries like piscina provide a ready-made, well-tested worker pool implementation.
  • A pool assigns incoming tasks to idle workers and queues tasks when all workers are busy.
  • Pool size is often set close to the number of available CPU cores (os.cpus().length).

Sharing Memory With SharedArrayBuffer

By default, data passed between the main thread and workers is copied. For performance-critical cases involving large binary data, SharedArrayBuffer allows multiple threads to read and write the same underlying memory directly, avoiding copy overhead, at the cost of needing careful synchronization to avoid race conditions.

Common Mistakes

  • Reaching for Worker Threads to solve I/O-bound waiting, which regular async/await already handles efficiently.
  • Creating a brand-new worker for every small task instead of reusing a pool.
  • Forgetting to handle the worker's 'error' event, risking an unhandled crash.
  • Assuming data is shared by reference between threads by default, it's copied unless using SharedArrayBuffer.

Key Takeaways

  • Worker Threads run genuinely CPU-bound JavaScript on separate threads, each with its own event loop.
  • They solve CPU-bound blocking, not I/O-bound waiting, which async/await already handles well.
  • Communication happens via message passing (postMessage), with data copied by default.
  • A worker pool avoids repeated worker-creation overhead for frequent CPU-heavy tasks.

Pro Tip

Before reaching for Worker Threads, confirm the bottleneck is actually CPU-bound (profile it first). Adding worker thread complexity to solve what's actually a slow database query or blocking I/O call adds overhead without fixing the real problem.