Node.js Cluster Module Deep Dive — Complete Guide to Multi-Process Architecture
In this tutorial, you will learn about Node.js Cluster Module Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.
Node.js cluster module creates multiple worker processes sharing the same server port, enabling multi-core utilization and horizontal scaling for HTTP applications.
What You'll Learn
By the end of this tutorial, you'll implement cluster mode with master-worker architecture, handle sticky sessions, perform zero-downtime restarts, manage shared state, and monitor worker health.
Why Cluster Matters
Node.js runs on one CPU core by default. Cluster mode uses all available cores, multiplying throughput without code changes.
Real-World Use
An Express API server on an 8-core machine forks 8 workers. Each handles requests independently. If one worker crashes, the master forks a replacement immediately.
Cluster Path
flowchart LR
A[Child Process] --> B[Cluster Module]
B --> C[Worker Threads]
C --> D[PM2]
D --> E[Scaling]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Basic Cluster Setup
Create a master Process that forks workers for each CPU core.
const cluster = require("node:cluster");
const http = require("node:http");
const os = require("node:os");
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Primary ${process.pid} forking ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died. Forking replacement.`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(`Worker ${process.pid} handled request\n`);
}).listen(3000);
}
Worker Health Monitoring
Monitor worker ping/pong to detect and replace unresponsive workers.
if (cluster.isPrimary) {
const workers = new Map();
for (let i = 0; i < os.cpus().length; i++) {
const worker = cluster.fork();
workers.set(worker.id, { alive: true });
}
setInterval(() => {
for (const [id, worker] of cluster.workers) {
worker.send({ type: "ping" });
setTimeout(() => {
if (workers.get(worker.id)?.alive === false) {
console.log(`Worker ${worker.id} unresponsive, killing`);
worker.kill();
}
workers.set(worker.id, { alive: false });
}, 2000);
}
}, 10000);
cluster.workers.forEach((worker) => {
worker.on("message", (msg) => {
if (msg.type === "pong") {
workers.set(worker.id, { alive: true });
}
});
});
}
Zero-Downtime Restart
Restart workers one at a time without dropping connections.
if (cluster.isPrimary) {
function restartWorkers() {
const workers = Object.values(cluster.workers);
let index = 0;
function restartNext() {
if (index >= workers.length) {
console.log("All workers restarted");
return;
}
const worker = workers[index];
console.log(`Restarting worker ${worker.id}`);
const newWorker = cluster.fork();
newWorker.on("listening", () => {
worker.disconnect();
worker.kill();
index++;
setTimeout(restartNext, 1000);
});
}
restartNext();
}
}
Shared State Between Workers
Workers cannot share in-memory state. Use Redis or database for shared data.
const http = require("node:http");
const { createClient } = require("redis");
async function handleRequest(req, res) {
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
let count = await client.get("requestCount");
count = count ? parseInt(count) + 1 : 1;
await client.set("requestCount", count);
res.end(`Request #${count} handled by worker ${process.pid}`);
await client.disconnect();
}
Graceful Shutdown
Handle SIGTERM to close connections gracefully before worker exit.
if (cluster.isWorker) {
const server = http.createServer((req, res) => {
res.end(`Worker ${process.pid}\n`);
});
server.listen(3000);
process.on("SIGTERM", () => {
console.log(`Worker ${process.pid} shutting down`);
server.close(() => {
process.exit(0);
});
});
}
Common Mistakes
1. Forking More Workers Than CPU Cores
Additional workers cause context switching overhead. Fork one per CPU core.
2. Not Handling Worker Exit Events
Dead workers without replacement reduce capacity. Always fork replacements on exit.
3. Assuming In-Memory State Is Shared
Each worker has its own memory. Shared state requires external storage like Redis.
4. Ignoring Sticky Sessions
Round-robin Load Balancing breaks Websocket connections. Use sticky sessions or Redis for session state.
5. No Graceful Shutdown
Killing workers immediately drops active connections. Implement graceful shutdown.
Practice Questions
1. How many workers should you fork?
One per CPU core. The primary process does not handle requests, only manages workers.
2. How do workers share the same port?
The master creates the server and passes the handle to workers. The OS distributes connections.
3. What is sticky session and why is it needed?
Ensures requests from the same client go to the same worker. Required for in-memory sessions and WebSocket.
4. How do you perform zero-downtime restart?
Fork new workers before disconnecting old ones. Wait for new workers to listen before killing old.
5. Challenge: Implement a cluster with worker health checking and auto-restart.
if (cluster.isPrimary) {
const createWorker = () => {
const w = cluster.fork();
let timeout = setTimeout(() => { console.log("Worker unresponsive"); w.kill(); }, 5000);
w.on("message", (m) => { if (m === "pong") { clearTimeout(timeout); timeout = setTimeout(() => w.kill(), 5000); }});
w.on("exit", () => { clearTimeout(timeout); createWorker(); });
};
for (let i = 0; i < os.cpus().length; i++) createWorker();
}
FAQ
Mini Project: Cluster Manager with Graceful Restart
Build a cluster manager with health monitoring and rolling restarts.
const cluster = require("node:cluster");
const os = require("node:os");
if (cluster.isPrimary) {
class ClusterManager {
constructor() { this.workers = new Map(); }
start(count = os.cpus().length) {
for (let i = 0; i < count; i++) this.forkWorker();
this.startHealthCheck();
}
forkWorker() {
const worker = cluster.fork();
this.workers.set(worker.id, { worker, healthy: true });
worker.on("message", (msg) => {
if (msg === "healthy") this.workers.set(worker.id, { worker, healthy: true });
});
return worker;
}
startHealthCheck() {
setInterval(() => {
this.workers.forEach((entry, id) => {
if (!entry.healthy) {
console.log(`Worker ${id} unhealthy, restarting`);
entry.worker.kill();
this.forkWorker();
}
entry.healthy = false;
entry.worker.send("health_check");
});
}, 10000);
}
rollingRestart() {
const ids = [...this.workers.keys()];
ids.forEach((id, i) => {
setTimeout(() => {
const entry = this.workers.get(id);
if (entry) {
entry.worker.disconnect();
setTimeout(() => this.forkWorker(), 500);
}
}, i * 2000);
});
}
}
const manager = new ClusterManager();
manager.start();
}
What's Next
Node.js Worker Threads Node.js PM2 Node.js Docker
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro