Skip to content
Plurafy

Clustering and Load Balancing

Scaling Node.js applications

Utilize multi-core processors, distribute load across workers, and handle graceful shutdown.

The cluster module allows Node.js to spawn worker processes and distribute traffic among them for better performance.

const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isMaster) {
const numWorkers = os.cpus().length;
console.log(`Master process ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
// Handle worker exit
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Restart worker
});
} else {
// Worker process
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`Handled by worker ${process.pid}`);
});
server.listen(3000);
console.log(`Worker ${process.pid} started`);
}
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numWorkers = os.cpus().length;
const workers = [];
// Fork workers
for (let i = 0; i < numWorkers; i++) {
const worker = cluster.fork();
workers.push(worker);
}
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('Master shutting down gracefully');
workers.forEach(worker => {
worker.kill();
});
// Wait for workers to exit
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} exited`);
});
});
// Monitor metrics
setInterval(() => {
Object.values(cluster.workers).forEach(worker => {
console.log(`Worker ${worker.process.pid} active`);
});
}, 30000);
} else {
// Worker code
require('./app').start();
}
if (cluster.isMaster) {
let currentWorkers = [];
function spawnNewWorkers() {
for (let i = 0; i < numWorkers; i++) {
const worker = cluster.fork();
currentWorkers.push(worker);
}
}
// Listen for reload signal
process.on('SIGUSR2', async () => {
console.log('Reloading workers...');
const newWorkers = [];
// Spawn new workers
for (let i = 0; i < numWorkers; i++) {
newWorkers.push(cluster.fork());
}
// Wait for new workers to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
// Kill old workers
currentWorkers.forEach(worker => {
worker.kill();
});
currentWorkers = newWorkers;
console.log('Workers reloaded');
});
} else {
// Worker with graceful shutdown
let server;
process.on('SIGTERM', () => {
if (server) {
server.close(() => {
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
process.exit(1);
}, 30000);
}
});
server = http.createServer(handleRequest);
server.listen(3000);
}
// Round-robin (default) - distributes fairly
// Sticky session - keep related requests on same worker
if (cluster.isMaster) {
// Session affinity
const workers = [];
for (let i = 0; i < numWorkers; i++) {
workers.push(cluster.fork());
}
const server = http.createServer((req, res) => {
// Simple hash-based distribution
const sessionId = req.headers['x-session-id'];
const workerId = hashFunc(sessionId) % workers.length;
// Forward to worker
workers[workerId].send({ req, res });
});
server.listen(3000);
}
ecosystem.config.js
module.exports = {
apps: [{
name: 'api',
script: './app.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
},
error_file: './logs/error.log',
out_file: './logs/out.log',
merge_logs: true,
max_memory_restart: '1G',
max_restarts: 10,
min_uptime: '10s'
}]
};
Terminal window
# Commands
pm2 start ecosystem.config.js
pm2 start ecosystem.config.js --env production
pm2 reload api # Graceful reload
pm2 restart api # Restart all
pm2 kill api # Shutdown
pm2 logs api # Show logs
pm2 monit # Monitor
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
// Redis client
const redisClient = createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24
}
}));
// Sessions persisted in Redis, accessible to all workers
app.get('/profile', (req, res) => {
// Works on any worker
console.log(req.session.userId);
});
// Master monitors worker health
if (cluster.isMaster) {
const workers = [];
for (let i = 0; i < numWorkers; i++) {
workers.push(cluster.fork());
}
// Periodic health check
setInterval(() => {
workers.forEach((worker, index) => {
worker.send({ cmd: 'health-check' });
const timeout = setTimeout(() => {
console.log(`Worker ${worker.process.pid} unresponsive`);
worker.kill();
workers[index] = cluster.fork();
}, 5000);
worker.on('message', (msg) => {
if (msg.cmd === 'health-check-response') {
clearTimeout(timeout);
}
});
});
}, 10000);
}
// Worker responds to health checks
if (cluster.isWorker) {
process.on('message', (msg) => {
if (msg.cmd === 'health-check') {
process.send({ cmd: 'health-check-response' });
}
});
}