Difficulty: Advanced
How do you utilize multiple CPU cores in Node.js? Explain the cluster module and PM2.
Node.js runs on a single thread by default, using only one CPU core. The cluster module allows you to fork multiple worker processes that share the same server port, utilizing all available CPU cores. The master process manages workers and distributes connections.
PM2 is a production process manager that handles clustering, auto-restart on crash, log management, monitoring, and zero-downtime reloads. It is the standard way to run Node.js in production. PM2's cluster mode is simpler than using the cluster module directly.
With cluster mode, each worker is a separate Node.js process with its own memory space and event loop. They share the TCP port through the OS. The master process distributes incoming connections using round-robin (default on Linux) or OS-level load balancing.
Workers are independent: if one crashes, others continue serving requests. The master can restart crashed workers automatically. This provides fault tolerance and better resource utilization. However, workers do not share memory, so shared state (sessions, cache) must use Redis or similar external stores.
const cluster = require('cluster');
const os = require('os');
const express = require('express');
const numCPUs = os.cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} starting ${numCPUs} workers`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// Restart crashed workers
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died (${signal || code})`);
console.log('Starting replacement worker...');
cluster.fork();
});
// Track workers
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} is online`);
});
} else {
// Workers share the same port
const app = express();
app.get('/', (req, res) => {
res.json({
pid: process.pid,
uptime: process.uptime(),
});
});
app.listen(3000, () => {
console.log(`Worker ${process.pid} listening on port 3000`);
});
}
The primary process forks workers equal to CPU count. Each worker runs the Express app. If a worker crashes, the primary forks a replacement.
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-api',
script: './dist/index.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster', // Enable cluster mode
max_memory_restart: '500M',
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
// Log configuration
error_file: './logs/error.log',
out_file: './logs/output.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss',
// Auto-restart
watch: false,
max_restarts: 10,
restart_delay: 4000,
}],
};
// PM2 Commands:
// pm2 start ecosystem.config.js --env production
// pm2 reload my-api # Zero-downtime restart
// pm2 scale my-api +2 # Add 2 more workers
// pm2 monit # Real-time monitoring
// pm2 logs my-api # View logs
// pm2 save # Save process list
// pm2 startup # Auto-start on boot
PM2's ecosystem file configures clustering, memory limits, logs, and environment variables. 'reload' provides zero-downtime restarts by restarting workers one at a time.
// Problem: workers don't share memory
// In-memory sessions/cache won't work with clustering
// BAD: in-memory session (each worker has different sessions)
const sessions = new Map(); // Only exists in THIS worker's memory
// GOOD: Redis for shared state across workers
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 86400000 },
}));
// For Socket.IO with clustering
const { createAdapter } = require('@socket.io/redis-adapter');
const pubClient = new Redis(process.env.REDIS_URL);
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
// Now Socket.IO events are shared across all workers
Workers are separate processes with separate memory. Use Redis for sessions, cache, and Socket.IO state to share across workers.
Cluster, PM2, Load Balancing, Multi-Core, Process Management