Cluster Module
Scale Node.js HTTP servers across all CPU cores using the built-in cluster module
Overview
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.
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4