Node.js Worker Threads: Offloading CPU-Intensive Work

Node.js is single-threaded by design, but Worker Threads let you parallelize CPU-heavy tasks without blocking the event loop. Learn when to use workers, how to structure thread pools, and how to avoid common pitfalls.

Node.js Worker Threads: Offloading CPU-Intensive Work

The Event Loop Is Not a CPU Scheduler

Node.js excels at I/O-bound workloads: database queries, HTTP requests, file reads. Its non-blocking event loop keeps thousands of concurrent connections responsive. But the moment you run a synchronous CPU-heavy task, such as image resizing, PDF parsing, bcrypt with high cost factors, or large JSON serialization, you block the entire loop. Every pending request waits.

Worker Threads vs. Child Processes

The worker_threads module spawns lightweight threads that share memory via SharedArrayBuffer (when enabled) or message passing. Unlike child_process, workers start faster and can share typed arrays for high-throughput data pipelines. Child processes remain better for running entirely separate programs or isolating crashes.

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

export function hashPassword(password: string): Promise {
  return new Promise((resolve, reject) => {
    const worker = new Worker(path.join(import.meta.dirname, 'hash-worker.js'), {
      workerData: { password, rounds: 12 },
    });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}
// hash-worker.js
import { parentPort, workerData } from 'worker_threads';
import bcrypt from 'bcrypt';

const hash = bcrypt.hashSync(workerData.password, workerData.rounds);
parentPort.postMessage(hash);

Designing a Worker Pool

Spawning a new worker per request adds startup overhead. Production systems use a fixed pool (often sized to os.cpus().length - 1) and queue tasks. Libraries like Piscina wrap this pattern with backpressure and graceful shutdown.

  • Keep workers stateless: pass input via workerData or messages; avoid shared mutable globals.
  • Serialize carefully: large objects copy across the thread boundary unless you use transferables.
  • Handle failures: workers can exit unexpectedly; the pool must replace them and surface errors to callers.

Observability and Limits

Monitor queue depth, worker utilization, and p99 latency. Worker threads solve CPU bottlenecks. They do not replace horizontal scaling. For embarrassingly parallel batch jobs, consider dedicated job queues (BullMQ, SQS) with worker fleets rather than in-process pools on every API server.

Used judiciously, Worker Threads let Node.js remain the right tool for full-stack APIs that occasionally need serious computation without surrendering responsiveness.

Explore Topics

#Node.js#Worker Threads#Event Loop#CPU Optimization#Backend Performance#JavaScript#Multithreading