Skip to content

Instantly share code, notes, and snippets.

@SeLub
Created October 2, 2025 10:42
Show Gist options
  • Select an option

  • Save SeLub/2455aef4f30096f4a75025cd950d68bc to your computer and use it in GitHub Desktop.

Select an option

Save SeLub/2455aef4f30096f4a75025cd950d68bc to your computer and use it in GitHub Desktop.
How Clustering Works in Node.js

When using clusters in Node.js, it's important to understand how clustering works and whether or not a load balancer is necessary. Clustering allows you to create multiple server instances that share the same port, which can be beneficial for utilizing multi-core systems. However, this does not inherently provide any load balancing capabilities on its own.

How Clustering Works in Node.js

When you use cluster module in Node.js, it forks several child processes to handle incoming connections. Each fork listens on the same port and can receive requests for different parts of your application. Here's a basic example:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

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

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // Here, it is an HTTP server
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}

Spread Requests Between Forks

Without a load balancer, each fork will handle requests independently. If you want to balance the load between these forks, you can implement custom logic or use an external load balancer. Here are some strategies:

  1. Round-Robin: Distribute incoming connections in a round-robin fashion across the available workers.
  2. Least Connections: Direct new connections to the worker with the fewest active connections.
  3. Consistent Hashing: Use consistent hashing to route requests to specific workers based on a hash of the request data.
  4. Weighted Round-Robin or Weighted Least Connections: Assign weights to different workers and distribute connections according to these weights.
  5. External Load Balancer: Use an external service like Nginx, HAProxy, or AWS ELB to distribute traffic across your Node.js clusters. This is a common practice in production environments.

Example: Round-Robin Strategy

Here's a simple example using the round-robin strategy with cluster module:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

  // Round-robin strategy
  let currentIndex = 0;

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    // Optional: Bring up a new worker to replace the dead one
    if (!cluster.workers[currentIndex]) {
      currentIndex = 0;
    }
    cluster.fork();
  });

  setInterval(() => {
    const workers = Object.values(cluster.workers);
    const worker = workers[currentIndex % workers.length];
    worker.send('ping');
    currentIndex++;
  }, 1000);
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);

  process.on('message', (msg) => {
    if (msg === 'ping') {
      // Optional: Respond to the master for health checks or other signals
      process.send('pong');
    }
  });
}

Conclusion

While Node.js clusters allow multiple processes to share a port, they do not inherently provide load balancing. To effectively distribute incoming requests across multiple workers, you can implement custom strategies like round-robin or use an external load balancer for better control and performance.

@SeLub
Copy link
Author

SeLub commented Oct 2, 2025

Your system handled 44k req/sec easily! That's a powerful setup. Let's push it harder:

load-test.cjs

const http = require('http');

const TARGET_URL = 'http://localhost:8000';
const REQUESTS_PER_SECOND = 200000;
const DURATION_SECONDS = 30;
const BATCH_SIZE = 2000;
const BATCH_INTERVAL = Math.floor(1000 / (REQUESTS_PER_SECOND / BATCH_SIZE));

let requestCount = 0;
let errorCount = 0;

function makeRequest() {
  const req = http.request(TARGET_URL, (res) => {
    requestCount++;
  });

  req.on('error', () => {
    errorCount++;
  });

  req.end();
}

console.log(`Starting load test: ${REQUESTS_PER_SECOND} requests/sec for ${DURATION_SECONDS}s`);
console.log(`Batch size: ${BATCH_SIZE}, Interval: ${BATCH_INTERVAL}ms`);

const interval = setInterval(() => {
  for (let i = 0; i < BATCH_SIZE; i++) {
    makeRequest();
  }
  if (requestCount % 10000 === 0) {
    console.log(`Sent: ${requestCount}, Errors: ${errorCount}`);
  }
}, BATCH_INTERVAL);

setTimeout(() => {
  clearInterval(interval);
  console.log(`\nTest completed: ${requestCount} requests, ${errorCount} errors`);
}, DURATION_SECONDS * 1000);

Now it will attempt 200,000 requests/second for 30 seconds.

For even more stress, create multiple test instances:

stress-test.sh

#!/bin/bash

echo "Starting 4 parallel load tests..."

node load-test.cjs &
node load-test.cjs &
node load-test.cjs &
node load-test.cjs &

wait
echo "All tests completed"

Run with:

chmod +x stress-test.sh
./stress-test.sh

This will launch 4 parallel instances = 800k requests/second total. Monitor with htop to see if you can finally stress that beast of a machine!

Your i7-14700 (20 cores/28 threads) with 64GB DDR5-6000 should handle serious load before breaking a sweat.

RESULT:
Screenshot from 2025-10-02 14-51-56

./stress-test.sh
Starting 4 parallel load tests...
Starting load test: 200000 requests/sec for 30s
Starting load test: 200000 requests/sec for 30s
Starting load test: 200000 requests/sec for 30s
Batch size: 2000, Interval: 10ms
Batch size: 2000, Interval: 10ms
Batch size: 2000, Interval: 10ms
Starting load test: 200000 requests/sec for 30s
Batch size: 2000, Interval: 10ms
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 0, Errors: 0
Sent: 10000, Errors: 0
Sent: 10000, Errors: 1244
Sent: 10000, Errors: 1299

Test completed: 39709 requests, 3167 errors

Test completed: 39238 requests, 3523 errors

Test completed: 41456 requests, 3377 errors

Test completed: 37749 requests, 3518 errors
All tests completed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment