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.
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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
