Mastering Node.js Multi‑Process: Load Balancing, Graceful Shutdown & IPC

This article explains how to build a robust multi‑process Node.js server using the cluster module, covering round‑robin load balancing, graceful worker shutdown, process monitoring, and inter‑process communication with code examples for both master and worker processes.

Node Underground
Node Underground
Node Underground
Mastering Node.js Multi‑Process: Load Balancing, Graceful Shutdown & IPC

The previous article introduced common problems when deploying multiple processes in Node.js; this piece shows how to use the cluster module to achieve load balancing, graceful shutdown, process guarding, and inter‑process communication (IPC) in production.

Load Balancing

In the simple multi‑process model, each worker binds the same port and competes for incoming connections, causing uneven load. The round‑robin approach lets the master process own the listening socket, accept connections, and explicitly dispatch them to workers, ensuring controllable distribution.

Load balancing diagram
Load balancing diagram

Simple demo of a master process that forks workers and forwards accepted connections:

const net = require('net');
const fork = require('child_process').fork;
var workers = [];
for (var i = 0; i < 4; i++) {
  workers.push(fork('./worker'));
}
var handle = net._createServerHandle('0.0.0.0', 3000);
handle.listen();
handle.onconnection = function (err, handle) {
  var worker = workers.pop();
  worker.send({}, handle);
  workers.unshift(worker);
};

Worker process handling the transferred socket:

const net = require('net');
process.on('message', function (m, handle) {
  start(handle);
});
var buf = 'hello Node.js';
var res = ['HTTP/1.1 200 OK', 'content-length:' + buf.length].join('
') + '

' + buf;
function start(handle) {
  console.log('got a connection on worker, pid = %d', process.pid);
  var socket = new net.Socket({ handle: handle });
  socket.readable = socket.writable = true;
  socket.end(res);
}

Graceful Shutdown

When a worker encounters an uncaught exception, it should stop accepting new requests, finish processing ongoing ones, and then exit, allowing the master to fork a replacement.

setTimeout(function () {
  process.exit(1);
}, timeout);

Before closing the server, disable keep‑alive on incoming requests so clients know the connection will close:

server.on('request', function (req, res) {
  req.shouldKeepAlive = false;
  res.shouldKeepAlive = false;
  if (!res._header) {
    res.setHeader('Connection', 'close');
  }
});

Process Guard

The master monitors worker exits or disconnections and automatically forks new workers to maintain stability.

cluster.on('exit', function () {
  cluster.fork();
});
cluster.on('disconnect', function () {
  cluster.fork();
});

Third‑party modules such as recluster and cfork provide mature implementations.

IPC (Inter‑Process Communication)

Node.js uses a pipe created by libuv for communication between the parent and child processes. Two modes exist: sending file descriptors (fd) and sending plain messages without fd.

Sending fd

The master can pass a client socket’s fd to a worker, allowing the worker to handle the connection directly.

Not sending fd

When only simple strings are exchanged, communication is bidirectional but requires two pipes for full duplex; libuv instead uses a socketpair to create a true duplex channel.

Node.js creates the pipe via: new process.binding('pipe_wrap').Pipe(true); The pipe is initially closed; libuv’s uv_spawn binds a real socketpair to it, and the fd is passed to the child through the environment variable NODE_CHANNEL_FD:

options.envPairs.push('NODE_CHANNEL_FD=' + ipcFd);

In the child, the fd is retrieved and opened:

var fd = parseInt(process.env.NODE_CHANNEL_FD, 10);
var p = new Pipe(true);
p.open(fd);

Demo showing master and worker communicating via the channel:

master.js

const WriteWrap = process.binding('stream_wrap').WriteWrap;
var cp = require('child_process');
var worker = cp.fork(__dirname + '/worker.js');
var channel = worker._channel;
channel.onread = function (len, buf, handle) {
  if (buf) {
    console.log(buf.toString());
    channel.close();
  } else {
    console.log('channel closed');
    channel.close();
  }
};
var message = { hello: 'worker', pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '
';
channel.writeUtf8String(req, string, null);

worker.js

const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;
channel.ref();
channel.onread = function (len, buf, handle) {
  if (buf) {
    console.log(buf.toString());
  } else {
    console.log('channel closed');
    process._channel.close();
  }
};
var message = { hello: 'master', pid: process.pid };
var req = new WriteWrap();
var string = JSON.stringify(message) + '
';
channel.writeUtf8String(req, string, null);

Process Disconnect

When a worker calls disconnect(), the IPC channel closes, triggering the master’s disconnect event, after which the master can fork a new worker.

Demo:

master.js

const WriteWrap = process.binding('stream_wrap').WriteWrap;
const net = require('net');
const fork = require('child_process').fork;
var workers = [];
for (var i = 0; i < 4; i++) {
  var worker = fork(__dirname + '/worker.js');
  worker.on('disconnect', function () {
    console.log('[%s] worker %s is disconnected', process.pid, worker.pid);
  });
  workers.push(worker);
}
var handle = net._createServerHandle('0.0.0.0', 3000);
handle.listen();
handle.onconnection = function (err, handle) {
  var worker = workers.pop();
  var channel = worker._channel;
  var req = new WriteWrap();
  channel.writeUtf8String(req, 'dispatch handle', handle);
  workers.unshift(worker);
};

worker.js

const net = require('net');
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;
var buf = 'hello Node.js';
var res = ['HTTP/1.1 200 OK', 'content-length:' + buf.length].join('
') + '

' + buf;
channel.ref();
channel.onread = function (len, buf, handle) {
  console.log('[%s] worker %s got a connection', process.pid, process.pid);
  var socket = new net.Socket({ handle: handle });
  socket.readable = socket.writable = true;
  socket.end(res);
  console.log('[%s] worker %s is going to disconnect', process.pid, process.pid);
  channel.close();
};

Simple Load‑Balancing Server

Combining the concepts, the following minimal code runs a multi‑process HTTP server that distributes connections via the master and workers using fd passing.

master.js

const WriteWrap = process.binding('stream_wrap').WriteWrap;
const net = require('net');
const fork = require('child_process').fork;
var workers = [];
for (var i = 0; i < 4; i++) {
  workers.push(fork(__dirname + '/worker.js'));
}
var handle = net._createServerHandle('0.0.0.0', 3000);
handle.listen();
handle.onconnection = function (err, handle) {
  var worker = workers.pop();
  var channel = worker._channel;
  var req = new WriteWrap();
  channel.writeUtf8String(req, 'dispatch handle', handle);
  workers.unshift(worker);
};

worker.js

const net = require('net');
const WriteWrap = process.binding('stream_wrap').WriteWrap;
const channel = process._channel;
var buf = 'hello Node.js';
var res = ['HTTP/1.1 200 OK', 'content-length:' + buf.length].join('
') + '

' + buf;
channel.ref();
channel.onread = function (len, buf, handle) {
  var socket = new net.Socket({ handle: handle });
  socket.readable = socket.writable = true;
  socket.end(res);
};

Conclusion

The Node.js cluster module provides a straightforward way to build a scalable, fault‑tolerant multi‑process server, handling load balancing, graceful worker exit, process monitoring, and IPC without requiring external tools.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

load balancingNode.jsmulti-processClusterIPCGraceful Shutdown
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.