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: