How to Scale Node.js with Cluster Module
Node.js runs on a single thread by default, which means it cannot
naturally utilize all the cores of a modern multi-core server. This
article explains how to scale your Node.js application using its
built-in cluster module to run multiple instances of your
app simultaneously. You will learn how the cluster module works, see a
practical code implementation, and discover how to handle worker process
crashes to ensure high availability.
Understanding the Node.js Cluster Module
The built-in cluster module allows you to create a
parent-child relationship between processes. It spawns a master (or
primary) process that is responsible for orchestrating worker
processes.
Each worker process runs on its own thread, has its own memory space, and shares the same server ports with other workers. The primary process distributes incoming HTTP requests to the worker processes using a round-robin load-balancing algorithm, which is the default on most operating systems.
Implementing the Cluster Module
To implement clustering, you must check if the current process is the primary process. If it is, you fork the process based on the number of available CPU cores. If it is a worker process, you start your standard application server.
Here is a complete, production-ready example using modern Node.js APIs:
const cluster = require('node:cluster');
const http = require('node:http');
const { availableParallelism } = require('node:os');
const process = require('node:process');
// Get the total number of CPU cores available
const numCPUs = availableParallelism();
if (cluster.isPrimary) {
console.log(`Primary process ${process.pid} is running.`);
// Fork workers matching the number of CPU cores
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Listen for dying workers and spawn new ones
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker process ${worker.process.pid} died. Spawning a replacement...`);
cluster.fork();
});
} else {
// Worker processes handle incoming HTTP requests
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Handled by worker process ${process.pid}\n`);
}).listen(8000);
console.log(`Worker process ${process.pid} started.`);
}Managing Worker Failures
In a production environment, worker processes might crash due to unhandled exceptions, memory leaks, or runtime errors.
The implementation above handles this by listening to the
exit event on the cluster object. When a
worker dies, cluster.on('exit') is triggered. By calling
cluster.fork() inside this event listener, you immediately
replace the dead worker with a new one, keeping your application at
maximum capacity and preventing downtime.
Key Considerations for Clustered Applications
When scaling horizontally on a single machine using the cluster module, keep the following architectural constraints in mind:
- Statelessness: Because requests are distributed randomly across different processes, you cannot store session data in local application memory. Use an external, shared in-memory database like Redis to manage sessions and application state.
- Database Connections: Each worker process maintains its own database connection pool. Ensure your database server can handle the increased number of concurrent connections generated by multiplying your pool size by the number of CPU cores.
- Shared Ports: You do not need to worry about port conflicts. The primary process intercepts external traffic on the designated port (e.g., port 8000) and handles the distribution to workers internally.