How to Scale Node.js Applications Using the Cluster Module

Node.js Performance Guide: Utilizing All CPU Cores with Cluster

Tung Nguyen 5 min read
How to Scale Node.js Applications Using the Cluster Module

In this article, I will walk you through the easiest way to scale up your Node.js application to serve more requests: using the Cluster module.

Node.js runs on a single thread, which means that by default it uses only one CPU core. When your application receives a large number of requests or executes long-running, CPU-intensive tasks, the event loop can become blocked. As a result, the entire application may appear unresponsive—even though the server still has multiple idle CPU cores available.

But what happens if your server has many CPU cores? How can you take advantage of all of them?

The Problem: The Single-Threaded Bottleneck

Let’s look at a practical example. Below is a simple Node.js application with two endpoints:

  1. /fib: Calculates the 50th Fibonacci number. This is a CPU-bound, blocking task.

  2. /info: Returns basic server information. This is a lightweight task.

const express = require("express");
const os = require("os");

const app = express();
const PORT = 3000;

// Simple recursive Fibonacci (not optimized for large numbers)
// This simulates a heavy CPU task
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

// Endpoint to return the 50th Fibonacci number
app.get("/fib", (req, res) => {
  const start = process.hrtime();
  const result = fib(50); // This blocks the event loop!
  const end = process.hrtime(start);
  const executionTime = (end[0] * 1000 + end[1] / 1000000).toFixed(2); // Convert to milliseconds

  res.json({
    fibonacciNumber: result,
    processingTime: `${executionTime}ms`,
    processId: process.pid,
  });
});

// Endpoint to return server info
app.get("/info", (req, res) => {
  res.json({
    name: os.hostname(),
    numberOfCpus: os.cpus().length,
    processId: process.pid,
  });
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

Because the Fibonacci function is poorly optimized and CPU-intensive, it occupies the entire thread while executing. During this time, the application cannot respond to any other requests.

If you try to hit the /info endpoint while the /fib calculation is running, the server won’t respond until the calculation is finished.

The result: Your application becomes unresponsive when handling heavy tasks, leading to a poor user experience.

The lightweight /info endpoint cannot respond until the server finishes the blocking Fibonacci calculation running on /fib

If you look at your system resources during this process, you will see a frustrating sight: on a server with 10 CPU threads, one thread is at 100% usage, while the other 9 are sitting idle.

CPU-Bound Work in Node.js: One Thread at 100%, System Mostly Idle

Obviously, we have 9 idle CPUs doing nothing while one CPU works itself to death. So, how do we utilize those idle resources to handle more requests?

The Solution: Node.js Cluster

This is where the Cluster module comes to the rescue.

What is the Node.js Cluster?

The Node.js Cluster module allows you to create child processes (workers) that run simultaneously on multiple CPU cores but share the same server port.

  • Utilization: It allows you to scale your application across all available CPU cores.

  • Independence: Each worker has its own event loop and memory instance.

  • Load Balancing: The Master (primary) process acts as a load balancer. It manages the workers and coordinates them, but it does not handle the incoming HTTP requests itself—the workers do that.

Implementing the Cluster

With a bit of refactoring, we can transform our code to utilize the full power of the server:

const express = require("express");
const os = require("os");
const cluster = require("cluster");

// Check if the current process is the Primary (Master)
if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Primary ${process.pid} is running`);

  // Fork workers for each CPU core
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  // Monitor workers: if one dies, fork a new one
  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();
  });

} else {
  // This block runs inside the Worker processes
  const app = express();
  const PORT = 3000;

  function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
  }

  app.get("/fib", (req, res) => {
    const start = process.hrtime();
    const result = fib(50);
    const end = process.hrtime(start);
    const executionTime = (end[0] * 1000 + end[1] / 1000000).toFixed(2);

    res.json({
      fibonacciNumber: result,
      processingTime: `${executionTime}ms`,
      processId: process.pid,
    });
  });

  app.get("/info", (req, res) => {
    res.json({
      name: os.hostname(),
      numberOfCpus: os.cpus().length,
      processId: process.pid,
    });
  });

  app.listen(PORT, () => {
    console.log(`Worker ${process.pid} is running on http://localhost:${PORT}`);
  });
}

The Results

In this version, the code runs on multiple threads equal to the number of CPUs in your system.

Now, if you perform requests simultaneously:

Bash

GET localhost:3000/fib
GET localhost:3000/info

They can be processed at the same time! Even if one worker is 100% occupied calculating the Fibonacci number, the /info request will be handled instantly by a different worker on a different thread.

Cluster Mode: Blocking /fib Does Not Block Other Requests

You will also notice that the processId in the response changes, confirming that different workers are handling different requests.

Conclusion

Using the Cluster module is a powerful native way to maximize the hardware you are already paying for. By forking workers, you ensure that a single heavy request doesn’t bring your entire application to a halt.

Get the full source code here:

https://github.com/mt26691/dalabs-sample-files/tree/main/nodejs-cluster-example

software-development

Keep reading