Skip to content

Node.js Cluster — Complete Guide to Multi-Core Processing and Load Balancing

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Node.js Cluster. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js cluster module creates child processes (workers) that share server ports, enabling multi-core CPU utilization and horizontal scaling for HTTP applications.

What You'll Learn

By the end of this tutorial, you'll create clustered Node.js servers, understand load balancing, handle worker lifecycle, implement zero-downtime restarts, and monitor worker health.

Why Cluster Module Matters

Node.js runs in a single thread by default. On a server with 8 CPU cores, one core does all the work while 7 sit idle. The cluster module spreads incoming connections across all cores.

Real-World Use

An Express.js API server running on a 4-core machine spawns 4 workers. Each handles ~25% of incoming requests, quadrupling throughput compared to a single Process.

Cluster Learning Path

flowchart LR
  A[Child Process] --> B[Cluster]
  B --> C[Worker Threads]
  C --> D[Error Handling]
  D --> E[Deployment]
  A --> F{You Are Here}
  style F fill:#f90,color:#fff

Basic Cluster Setup

import cluster from "node:cluster";
import http from "node:http";
import { cpus } from "node:os";

if (cluster.isPrimary) {
  const numCPUs = cpus().length;
  console.log(`Primary ${process.pid} starting ${numCPUs} workers`);
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}\n`);
  }).listen(8000);
  console.log(`Worker ${process.pid} started`);
}

Worker Lifecycle

if (cluster.isPrimary) {
  cluster.on("fork", (worker) => console.log(`Worker ${worker.process.pid} forked`));
  cluster.on("online", (worker) => console.log(`Worker ${worker.process.pid} is online`));
  cluster.on("listening", (worker, address) =>
    console.log(`Worker ${worker.process.pid} listening on ${address.port}`)
  );
  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (code ${code}). Restarting...`);
    cluster.fork();
  });
  for (let i = 0; i < cpus().length; i++) cluster.fork();
}

Zero-Downtime Restart

Gracefully restart workers one at a time to avoid dropping requests.

import cluster from "node:cluster";
if (cluster.isPrimary) {
  const restartWorker = (workerIndex) => {
    const worker = Object.values(cluster.workers)[workerIndex];
    if (!worker) return;
    const newWorker = cluster.fork();
    newWorker.on("listening", () => {
      worker.disconnect();
      setTimeout(() => restartWorker(workerIndex + 1), 1000);
    });
  };
  // Send SIGUSR2 to trigger restart
  process.on("SIGUSR2", () => restartWorker(0));
}

Load Balancing Strategy

By default, on Linux, cluster uses round-robin distribution. On Windows, it uses shared sockets.

import cluster from "node:cluster";
cluster.schedulingPolicy = cluster.SCHED_RR;  // Force round-robin

Communicating with Workers

if (cluster.isPrimary) {
  cluster.on("message", (worker, message) => {
    console.log(`Message from worker ${worker.id}:`, message);
  });
  cluster.fork();
} else {
  process.send({ type: "ready", pid: process.pid });
}

Common Mistakes

1. Not Handling Worker Crashes

When a worker crashes, it stops handling requests. Always fork a replacement worker on 'exit' event.

2. Stateful Applications in Workers

Workers are separate processes with separate memory. Share state through a database or Redis, not in-memory variables.

3. Forking Workers Inside Workers

Only the primary process should call cluster.fork(). Workers should never fork new workers.

4. Ignoring the Scheduling Policy

On Windows, round-robin is not default. Set cluster.SCHED_RR for consistent behavior across platforms.

5. Not Using process.on('uncaughtException')

Workers should handle errors gracefully. An uncaught exception crashes the worker, but the primary should restart it.

Practice Questions

1. What is the cluster module's primary purpose?

To enable a Node.js application to use multiple CPU cores by forking worker processes that share server ports.

2. How does cluster distribute incoming connections?

On Linux, the default is round-robin (SCHED_RR). The primary accepts connections and distributes them to workers in turn.

3. Can workers share in-memory state?

No. Each worker is a separate process with its own memory. Use external storage (Redis, database) for shared state.

4. How do you implement zero-downtime restart?

Fork a new worker, wait for it to start listening, then disconnect the old worker. Repeat for all workers.

5. Challenge: Create a clustered HTTP server that logs which worker handles each request and automatically restarts crashed workers.

import cluster from "node:cluster";
import http from "node:http";
import { cpus } from "node:os";
if (cluster.isPrimary) {
  const numCPUs = cpus().length;
  for (let i = 0; i < numCPUs; i++) cluster.fork();
  cluster.on("exit", (worker) => { cluster.fork(); });
} else {
  http.createServer((req, res) => {
    res.end(JSON.stringify({ worker: process.pid, pid: process.ppid }));
  }).listen(3000);
}

FAQ

Does cluster work on Windows?

Yes, but the default scheduling is shared sockets. Set cluster.schedulingPolicy = cluster.SCHED_RR for round-robin.

How many workers should I create?

Typically CPU_COUNT workers. More workers than CPUs causes context switching overhead.

Is cluster suitable for WebSocket applications?

Yes, but sticky sessions are needed because WebSocket connections must stick to the same worker.

What is the difference between cluster and worker_threads?

Cluster forks separate processes (more isolation, more memory). Worker threads share the same process (less isolation, shared memory).

Can I use cluster with PM2?

PM2 has its own cluster mode. Use PM2's cluster mode instead of the cluster module when using PM2.

Mini Project: Clustered API Server

Build a clustered HTTP server with health checks and worker monitoring.

import cluster from "node:cluster";
import http from "node:http";
import { cpus } from "node:os";
const PORT = process.env.PORT || 3000;
if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} started`);
  for (let i = 0; i < cpus().length; i++) cluster.fork();
  cluster.on("exit", (worker) => {
    console.log(`Worker died, restarting`);
    cluster.fork();
  });
  setInterval(() => {
    const workerStatus = Object.values(cluster.workers).map(w => ({
      id: w.id, pid: w.process.pid, state: w.state
    }));
    console.log("Workers:", JSON.stringify(workerStatus));
  }, 5000);
} else {
  http.createServer((req, res) => {
    if (req.url === "/health") {
      res.end(JSON.stringify({ status: "ok", pid: process.pid }));
    } else {
      res.end(`Worker ${process.pid} says hello`);
    }
  }).listen(PORT);
}

What's Next

Node.js Worker Threads Node.js Error Handling Express.js

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro