PM2, Graceful Shutdown and Clustering
Node.js runs in a single thread by default. On a multi-core server, this means one CPU does all the work while the others idle. PM2 is the standard production process manager for Node.js: it runs your app as a cluster (one process per CPU core), restarts it automatically on crash, manages logs, and provides zero-downtime deployments. Graceful shutdown ensures that when your process needs to stop (deployment, crash recovery), it first finishes processing in-flight requests before terminating. Together, these three concerns — clustering, graceful shutdown, and process management — determine whether your application handles production load reliably.
Installing and Configuring PM2
PM2 is a global npm package. You define your application configuration in an ecosystem.config.js file which PM2 reads at startup. The key settings are instances (number of processes), exec_mode ('cluster' for multi-core), and autorestart (restart on crash).
npm install -g pm2// ecosystem.config.js
module.exports = {
apps: [{
name: 'cricket-api',
script: './server.js',
instances: 'max', // one per CPU core
exec_mode: 'cluster', // share the port across instances
watch: false, // never watch in production
max_memory_restart: '500M', // restart if process exceeds 500 MB
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000,
LOG_LEVEL: 'info'
},
error_file: './logs/pm2-error.log',
out_file: './logs/pm2-out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
}]
};# Start in production mode
pm2 start ecosystem.config.js --env production
# List running processes
pm2 list
# Zero-downtime reload (completes in-flight requests before restarting each worker)
pm2 reload cricket-api
# View logs
pm2 logs cricket-api
# Save process list to survive server reboots
pm2 save
pm2 startup # generates systemd/upstart startup scriptGraceful Shutdown
Graceful shutdown means that when a SIGTERM signal is received (sent by PM2 on reload, Kubernetes on pod termination, or Docker on container stop), your app stops accepting new connections, waits for in-flight requests to complete, closes database connections and message queue connections, and then exits cleanly. Without graceful shutdown, in-flight requests receive a connection reset error, database connections are abruptly closed (causing timeouts for the next connection attempt), and queued messages may be lost.
// server.js — graceful shutdown implementation
const app = require('./src/app');
const mongoose = require('mongoose');
const logger = require('./src/utils/logger');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
logger.info('Server started', { port: PORT, pid: process.pid });
});
// Handle graceful shutdown
async function shutdown(signal) {
logger.info('Shutdown signal received', { signal });
// 1. Stop accepting new connections
server.close(async () => {
logger.info('HTTP server closed — no new connections accepted');
try {
// 2. Close database connections
await mongoose.disconnect();
logger.info('Database disconnected');
// 3. Close queue workers (if any)
// await emailWorker.close();
logger.info('Graceful shutdown complete');
process.exit(0);
} catch (err) {
logger.error('Error during shutdown', { error: err.message });
process.exit(1);
}
});
// Safety net: force exit after 30 seconds if graceful shutdown hangs
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 30000).unref(); // .unref() prevents this timeout from keeping the process alive
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C in development
// Handle uncaught exceptions — log and exit
process.on('uncaughtException', (err) => {
logger.error('Uncaught exception', { error: err.message, stack: err.stack });
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled rejection', { reason: String(reason) });
process.exit(1);
});