Clustering

One Node process uses one CPU core; node:cluster starts a copy per core and they all accept connections on one port.

Syntaxcluster.fork([env]) -> cluster.Worker // callable in the primary process only

A process is one running copy of a program. The operating system gives it its own memory and its own numeric id, which Node exposes as process.pid. Your JavaScript runs on a single thread inside that process, so one Node process keeps one CPU core busy no matter how many cores the machine has.

node:cluster starts extra processes that all accept connections on the same port — the number clients connect to. The original process is the primary. The copies it starts are workers.

How it works

cluster.fork([env]) runs the same entry file again in a new process and returns a Worker object. In the copy, cluster.isPrimary is false, so one file can hold both branches. The primary owns the listening socket and hands each new connection to a worker. The default policy is round-robin: cluster.schedulingPolicy is cluster.SCHED_RR on every platform except Windows.

Shorthand and longhand

These two are the same code. The second only destructures the same functions off the module:

const cluster = require('node:cluster');
if (cluster.isPrimary) cluster.fork();

const { isPrimary, fork } = require('node:cluster');
if (isPrimary) fork();

What trips people up

Workers share no memory. An in-memory cache, a counter or a session object exists once per worker, so a value written in worker 1 is invisible in worker 2. Shared state has to live outside the processes.

Choose the count with os.availableParallelism() (added in v19.4.0 and v18.14.0) rather than os.cpus().length. cluster.isMaster is a deprecated alias for cluster.isPrimary, deprecated since v16.0.0.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Use all eight cores of a server with a single Node HTTP service instead of one core.
  • Keep serving requests when one worker crashes on a malformed payload.
  • Set the worker count from os.availableParallelism() so the same image fits a 2-core and a 16-core machine.
  • Replace workers one at a time during a code change so the port never stops accepting.

More examples

A clustered HTTP server

The shape almost every clustered service uses: one file, two branches, one port. Reach for it when a single Node process is pinned at 100% of one core while the other cores sit idle. Hitting the server repeatedly returns different pid values, which is how you confirm requests are really being spread.

Example · javascript
// server.js  ->  node server.js
const cluster = require('node:cluster');
const http = require('node:http');
const { availableParallelism } = require('node:os');

if (cluster.isPrimary) {
  // WEB_CONCURRENCY is an environment variable: a name/value pair the
  // process is started with, readable as process.env.WEB_CONCURRENCY.
  const count = Number(process.env.WEB_CONCURRENCY) || availableParallelism();
  console.log(`primary ${process.pid} starting ${count} workers`);
  for (let i = 0; i < count; i++) cluster.fork();
} else {
  // Every worker calls listen() on the same port. The primary accepts
  // the connections and passes each one to a worker.
  http.createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ pid: process.pid, url: req.url }));
  }).listen(3000);
}

Replacing dead workers, and stopping on purpose

Clustering only buys you resilience if a dead worker comes back. The shuttingDown flag matters: without it, the exit handler forks a replacement for each worker you are deliberately stopping, and the process never exits. Use this whenever the service is expected to survive an occasional crash.

Example · javascript
const cluster = require('node:cluster');
let shuttingDown = false;

if (cluster.isPrimary) {
  for (let i = 0; i < 4; i++) cluster.fork();

  // code is the exit status when the worker exited on its own;
  // signal is set instead when something killed it.
  cluster.on('exit', (worker, code, signal) => {
    if (shuttingDown) return;                 // expected during shutdown
    console.log(`worker ${worker.process.pid} gone (${signal || code})`);
    cluster.fork();                           // replace it
  });

  // SIGTERM is the signal an orchestrator sends to ask a process to stop.
  process.on('SIGTERM', () => {
    shuttingDown = true;
    // disconnect() asks every worker to stop accepting and close down.
    cluster.disconnect(() => process.exit(0));
  });
}

Giving each worker its own settings

Shows the destructured shorthand next to the plain cluster.* longhand, and the two ways the primary talks to workers: environment values fixed at fork time, and messages sent later. Useful when exactly one worker should own a scheduled job, or when each worker needs a distinct slot number.

Example · javascript
const cluster = require('node:cluster');
const { isPrimary, fork } = cluster;   // the two spellings, side by side

if (isPrimary) {
  // fork(env) adds these keys to that worker's process.env,
  // on top of everything the primary already had.
  for (let i = 0; i < 2; i++) fork({ WORKER_SLOT: String(i) });

  // cluster.workers is an object keyed by worker.id, primary only.
  for (const worker of Object.values(cluster.workers)) {
    worker.send('warm your cache');
  }
} else {
  console.log('slot', process.env.WORKER_SLOT);
  process.on('message', (msg) => console.log('from primary:', msg));
}

Discussion

  • Be the first to comment on this lesson.