Worker pools, shared memory and cluster

Node.js · Advanced · 16 min

One Node process uses one core for JavaScript; production servers usually need all of them.

There are two ways to use more than one CPU core from Node, and they solve different problems.

Worker thread pools speed up CPU-heavy tasks inside one process. Create N long-lived workers (N ≈ os.availableParallelism() minus one for the main thread), queue tasks, and hand each to an idle worker. A pool must handle a worker crashing (replace it, reject its task) and support shutdown. piscina is the standard library for this.

To avoid copying large data:

  • Transfer an ArrayBuffer (postMessage(buf, [buf])) — ownership moves, the sender's copy becomes unusable, zero copying.
  • Share a SharedArrayBuffer: both threads see the same memory. Coordinate with Atomics (Atomics.add, Atomics.wait/notify) because plain reads and writes race.
  • A MessageChannel gives two workers a direct port to talk without the main thread.

Cluster scales request handling. node:cluster forks the whole program into several processes; the primary process distributes incoming connections to workers (round-robin by default, except on Windows), and all of them appear to listen on the same port. Each worker is a full Node process with its own memory, so in-memory state — sessions, caches, rate-limit counters — is not shared. Move it to Redis or a database.

In containerised deployments, you'll often skip cluster entirely: run one Node process per container and let Kubernetes or the load balancer scale replicas. That gives simpler processes, better isolation and per-instance health checks. Cluster still fits single-VM deployments; PM2's cluster mode wraps the same module.

Whatever you choose, make the processes stateless so any instance can serve any request.

Example

// cluster-server.js — one HTTP worker per core
import cluster from "node:cluster";
import { createServer } from "node:http";
import { availableParallelism } from "node:os";

if (cluster.isPrimary) {
  const n = availableParallelism();
  console.log(`primary ${process.pid} forking ${n} workers`);
  for (let i = 0; i < n; i++) cluster.fork();

  cluster.on("exit", (worker, code) => {
    console.log(`worker ${worker.process.pid} exited (${code}); replacing`);
    cluster.fork();
  });
} else {
  let served = 0;                                   // per-process state, NOT shared!
  createServer((req, res) => {
    res.end(`pid ${process.pid} served ${++served}\n`);
  }).listen(3000);
}
// curl localhost:3000 repeatedly: different pids answer

Remember

  • Worker pools parallelise CPU tasks; cluster parallelises request handling
  • Transfer or share buffers to avoid copying large data; guard shared memory with Atomics
  • Cluster workers share nothing — keep state in external stores

Watch out for

  • Keeping sessions or rate limits in process memory, then adding cluster and getting inconsistent behaviour.

Check yourself

  1. After switching an app to cluster with 8 workers, logged-in users are randomly logged out. Likely cause?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/cluster.html