πŸš€AdvancedNode.js Concurrency

Cluster Module

Scale Node.js HTTP servers across all CPU cores using the built-in cluster module

Overview

A single Node.js process runs on one CPU core. On a modern server with 8, 16, or 32 cores, that means you are leaving the majority of compute capacity idle. The Cluster module solves this by letting you spawn multiple worker processes β€” one per CPU core β€” that all listen on the same port and share incoming connections, effectively distributing the HTTP load across all available cores. The cluster model works on a primary/worker architecture (the API uses isPrimary in Node 16+ and the legacy isMaster before that). The primary process calls cluster.fork() to create worker processes, each of which is a full copy of the current script running in a separate OS process. All workers bind to the same port; on Linux, the OS distributes incoming connections between workers using round-robin by default (Node also implements its own round-robin scheduler on non-Linux platforms). Because each worker is a separate process, there is no shared JavaScript state between them. If you need shared state (session data, rate-limit counters), you must use an external store like Redis. This stateless-per-process constraint is actually a feature in production β€” it forces architectural decisions that make your application easier to scale horizontally by adding more machines. The primary process can listen for worker events: `disconnect`, `exit`, and `online`. A robust cluster setup respawns workers when they crash β€” preventing a single unhandled exception from taking the entire server offline. This is how you get self-healing behavior in production without an external process manager.

Key Points

  • β€’cluster.isPrimary (or isMaster) determines if the current process is the primary coordinator
  • β€’cluster.fork() creates a worker that is a new copy of the current Node process
  • β€’All workers listen on the same port; the OS or Node distributes connections across them
  • β€’Workers are separate processes β€” no shared JS state; use Redis or similar for shared data
  • β€’The primary should respawn workers on "exit" events to provide self-healing behavior
  • β€’cluster.workers is a map of all current worker processes; you can message workers individually

Examples

Basic cluster setup β€” one worker per CPU core

const cluster = require('cluster');
const http    = require('http');
const os      = require('os');

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Primary ${process.pid} spawning ${numCPUs} workers`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (code: ${code}). Respawning...`);
    cluster.fork(); // Replace the dead worker
  });

} else {
  // Each worker process runs the HTTP server independently
  const server = http.createServer((req, res) => {
    res.end(`Hello from worker ${process.pid}`);
  });

  server.listen(3000, () => {
    console.log(`Worker ${process.pid} listening on port 3000`);
  });
}

On a 4-core machine this creates 4 server processes all handling port 3000. The OS load-balances connections between them.

Messaging between primary and workers

const cluster = require('cluster');
const os      = require('os');

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

    // Send task to specific worker
    worker.send({ task: 'compute', n: (i + 1) * 1_000_000 });

    // Receive result from worker
    worker.on('message', (msg) => {
      console.log(`Worker ${worker.id} result: ${msg.result}`);
    });
  }

} else {
  process.on('message', ({ task, n }) => {
    if (task === 'compute') {
      let sum = 0;
      for (let i = 1; i <= n; i++) sum += i;
      process.send({ result: sum });
    }
  });
}

Graceful shutdown β€” drain connections before stopping workers

const cluster = require('cluster');
const http    = require('http');
const os      = require('os');

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

  process.on('SIGTERM', () => {
    console.log('Primary received SIGTERM. Shutting down workers...');

    // Tell all workers to stop accepting new connections
    for (const id in cluster.workers) {
      cluster.workers[id].send('shutdown');
    }
  });

} else {
  const server = http.createServer((req, res) => res.end('OK'));
  server.listen(3000);

  process.on('message', (msg) => {
    if (msg === 'shutdown') {
      server.close(() => {
        console.log(`Worker ${process.pid} gracefully stopped`);
        process.exit(0);
      });
    }
  });
}

In production, combine this with a load balancer health-check that waits for all connections to drain before marking the server as down.

Zero-downtime reload β€” replace workers one at a time

const cluster = require('cluster');
const os      = require('os');

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  for (let i = 0; i < numCPUs; i++) cluster.fork();

  // Hot reload on SIGUSR2 β€” replace workers one at a time
  process.on('SIGUSR2', () => {
    const workers = Object.values(cluster.workers);
    let i = 0;

    function reloadNext() {
      if (i >= workers.length) return console.log('All workers reloaded');
      const worker = workers[i++];

      // Spawn new replacement, then kill old one after it is online
      const newWorker = cluster.fork();
      newWorker.on('online', () => {
        worker.kill('SIGTERM');
        reloadNext();
      });
    }

    reloadNext();
  });
}

Send SIGUSR2 to the primary process (kill -USR2 <pid>) to trigger a rolling restart with no dropped connections.

Common Mistakes

  • Storing application state in memory (session data, counters) β€” each worker has its own memory; use Redis or a database for shared state
  • Not respawning crashed workers β€” a cluster without respawn logic eventually runs out of workers as they crash
  • Using cluster for CPU-heavy work β€” for CPU tasks use Worker Threads; cluster is specifically for I/O-bound HTTP server scaling
  • Expecting in-process communication to work directly between workers β€” workers only communicate via the primary process as a message relay

Note

In production, consider using PM2 (`pm2 start server.js -i max`) which wraps cluster internally, adds process monitoring, zero-downtime reloads, log management, and auto-restart on crash. The cluster module is the foundation PM2 builds on β€” understanding it makes PM2 much less opaque.

Exercises

Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.

Loading editor…

Completed: 0 / 6 exercises

Knowledge Check

Question 1 of 4

How does the Cluster module enable multiple workers to listen on the same port?