Node.js Cluster Module for Multi-Core Scaling
Scale your Node.js application across all CPU cores using the cluster module. Covers load balancing, zero-downtime restarts, and production patterns.
What you'll learn
- ✓How the cluster module distributes work across CPU cores
- ✓Round-robin vs OS-level load balancing
- ✓Zero-downtime restart patterns
- ✓When to use cluster vs worker threads vs a process manager
Prerequisites
- •Basic Node.js knowledge
- •Understanding of processes and networking
- •Familiarity with HTTP servers
A single Node.js process uses one CPU core. If your server has 8 cores, you are wasting 87% of your hardware. The cluster module solves this by forking your application into multiple worker processes that share the same server port. Each worker is a full Node.js instance with its own event loop and memory, and the primary process distributes incoming connections among them.
How clustering works
The cluster module uses a primary-worker architecture. The primary process forks worker processes, and incoming TCP connections are distributed to workers by the operating system or by Node.js itself.
import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';
import http from 'node:http';
const numCPUs = availableParallelism();
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died (${signal || code})`);
console.log('Starting a new worker...');
cluster.fork(); // Auto-restart crashed workers
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end(`Handled by worker ${process.pid}\n`);
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
When you run this, the primary process forks workers equal to the number of CPU cores. All workers share port 8000. Incoming requests are distributed across workers automatically.
Load balancing strategies
Node.js supports two strategies for distributing connections:
Round-robin (default on Linux and macOS):
The primary process accepts connections and distributes them to workers in a round-robin fashion. This gives the most even distribution.
// Force round-robin scheduling
cluster.schedulingPolicy = cluster.SCHED_RR;
OS-level (default on Windows):
The OS kernel decides which worker handles each connection. This can lead to uneven distribution where some workers are overloaded while others sit idle.
// Let the OS decide
cluster.schedulingPolicy = cluster.SCHED_NONE;
For most applications, the default round-robin policy works well.
Communicating between primary and workers
Workers and the primary process communicate through IPC (inter-process communication) using message passing.
if (cluster.isPrimary) {
const worker = cluster.fork();
// Send a message to the worker
worker.send({ type: 'config', port: 8000 });
// Receive messages from workers
worker.on('message', (msg) => {
if (msg.type === 'stats') {
console.log(`Worker ${worker.id} handled ${msg.requestCount} requests`);
}
});
} else {
let requestCount = 0;
process.on('message', (msg) => {
if (msg.type === 'config') {
console.log('Received config:', msg);
}
});
// Report stats every 10 seconds
setInterval(() => {
process.send({ type: 'stats', requestCount });
}, 10_000);
http.createServer((req, res) => {
requestCount++;
res.end('OK');
}).listen(8000);
}
Remember that workers have separate memory spaces. You cannot share variables between them. Use Redis, a database, or message passing for shared state.
Zero-downtime restarts
One of the most valuable patterns with clustering is restarting workers one at a time to deploy new code without dropping connections.
import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';
if (cluster.isPrimary) {
const numCPUs = availableParallelism();
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
async function rollingRestart() {
const workers = Object.values(cluster.workers);
for (const worker of workers) {
// Fork a new worker before killing the old one
const replacement = cluster.fork();
await new Promise((resolve) => {
replacement.on('listening', resolve);
});
// New worker is ready — disconnect the old one
worker.disconnect();
await new Promise((resolve) => {
worker.on('exit', resolve);
});
console.log(`Replaced worker ${worker.process.pid} with ${replacement.process.pid}`);
}
console.log('Rolling restart complete');
}
// Trigger restart with SIGUSR2
process.on('SIGUSR2', () => {
console.log('Received SIGUSR2 — starting rolling restart');
rollingRestart();
});
cluster.on('exit', (worker, code) => {
if (code !== 0 && !worker.exitedAfterDisconnect) {
console.log(`Worker ${worker.process.pid} crashed — restarting`);
cluster.fork();
}
});
}
Send kill -USR2 <primary_pid> to trigger the restart. Each worker is replaced one at a time, so the server never has zero capacity.
Graceful shutdown with clustering
Workers should finish processing in-flight requests before exiting:
if (!cluster.isPrimary) {
const server = http.createServer((req, res) => {
// Simulate some work
setTimeout(() => {
res.end('Done');
}, 1000);
});
server.listen(8000);
process.on('message', (msg) => {
if (msg === 'shutdown') {
console.log(`Worker ${process.pid} shutting down gracefully`);
server.close(() => {
console.log(`Worker ${process.pid} closed all connections`);
process.exit(0);
});
// Force exit after 10 seconds
setTimeout(() => {
console.log(`Worker ${process.pid} forced exit`);
process.exit(1);
}, 10_000);
}
});
}
Sticky sessions for WebSocket
If your application uses WebSockets or any stateful protocol, you need sticky sessions so that all requests from the same client go to the same worker.
import cluster from 'node:cluster';
import net from 'node:net';
import { createHash } from 'node:crypto';
if (cluster.isPrimary) {
const workers = [];
for (let i = 0; i < 4; i++) {
workers.push(cluster.fork());
}
const server = net.createServer({ pauseOnConnect: true }, (connection) => {
// Hash the IP to pick a consistent worker
const ip = connection.remoteAddress;
const hash = createHash('md5').update(ip).digest();
const workerIndex = hash.readUInt32BE(0) % workers.length;
workers[workerIndex].send('sticky-session:connection', connection);
});
server.listen(8000);
}
Cluster vs worker threads vs process managers
| Feature | Cluster | Worker Threads | PM2 |
|---|---|---|---|
| Separate memory | Yes | Shared possible | Yes |
| Share server port | Yes | No | Yes |
| CPU parallelism | Yes | Yes | Yes |
| Zero-downtime restart | Manual | N/A | Built-in |
| Monitoring | Manual | Manual | Built-in |
Use cluster when you need to scale an HTTP server across cores and want full control. Use worker threads when you need to offload CPU-intensive computation from the main thread. Use PM2 when you want clustering with built-in monitoring, log management, and deployment features without writing the infrastructure yourself.
Production checklist
Before deploying a clustered Node.js application:
- Set the number of workers to
availableParallelism()or slightly less to leave room for the OS - Always auto-restart crashed workers in the primary process
- Implement graceful shutdown with a timeout
- Use shared state storage (Redis) instead of in-memory state
- Add health checks per worker
- Monitor individual worker memory and CPU usage
- Test your rolling restart procedure before deploying
Key takeaways
The cluster module is the simplest way to use all CPU cores with Node.js. It works best for stateless HTTP servers where each request is independent. For stateful applications, add sticky sessions. For CPU-bound work within a single request, combine clustering with worker threads. And in production, consider whether PM2 gives you what you need without custom code.
Related articles
- Node.js Node.js Clustering for Performance
Scale Node.js applications across CPU cores with the cluster module. Covers worker management, load balancing, zero-downtime restarts, and PM2.
- Node.js Node.js vs Deno vs Bun: JavaScript Runtimes Compared
Compare Node.js, Deno, and Bun on performance, security, compatibility, and developer experience. Choose the right JavaScript runtime for 2026.
- Node.js Corepack: Managing Package Managers in Node.js
Use Corepack to enforce consistent package manager versions across your team. Covers setup, configuration, and CI integration.
- Node.js Node.js diagnostics_channel: Built-in Observability
Use the diagnostics_channel API to add zero-overhead observability to your Node.js applications without modifying library code.