Process Management
Keep Node.js apps alive with PM2 — clustering, graceful shutdown, and logs.
Overview
In production, you need your Node.js application to stay running even when it crashes, utilize all CPU cores, and restart automatically when the server reboots. PM2 (Process Manager 2) is the industry-standard solution for this. It keeps your app alive by auto-restarting on crashes, distributes traffic across multiple worker processes (one per CPU core) using cluster mode, provides log management, and generates startup scripts for auto-start on boot. Install it globally: 'npm install -g pm2'.
Production project structure matters. A well-organized Node.js project separates concerns into folders: src/ (application code), config/ (environment config), routes/ (Express route files), utils/ (helper functions), middleware/ (auth, validation). The .env file stores environment-specific variables (DATABASE_URL, JWT_SECRET, PORT) — never commit this to Git. The .gitignore must include .env and node_modules/. Use ESLint and Prettier for consistent code formatting across the team.
PM2 cluster mode forks one worker process per CPU core, and all workers share the same port. PM2's load balancer distributes incoming connections across workers. When deploying a new version, 'pm2 reload' achieves zero-downtime: new workers start and accept traffic before old ones are terminated. Graceful shutdown handles SIGTERM by calling server.close() to stop accepting new connections, waiting for in-flight requests to finish, and then cleanly closing database connections before exiting.
Code Example
// ecosystem.config.js — PM2 configuration file
module.exports = {
apps: [{
name: 'devonix-api',
script: './src/server.js',
instances: 'max', // one per CPU core (auto-detect)
exec_mode: 'cluster', // fork mode uses Node cluster module
watch: false, // don't watch files (use reload instead)
max_memory_restart: '1G', // restart if memory exceeds 1GB
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
},
out_file: './logs/app.log',
error_file: './logs/err.log',
merge_logs: true, // merge logs from all cluster workers
log_date_format: 'YYYY-MM-DD HH:mm:ss',
restart_delay: 4000, // wait 4s before restart on crash
}]
}
// Usage:
// pm2 start ecosystem.config.js --env production
// pm2 reload ecosystem.config.js --env production (zero-downtime)
// pm2 startup && pm2 save (auto-start on reboot)
// ── src/server.js — Graceful Shutdown ────────────
const express = require('express')
const app = express()
// ... routes and middleware ...
const server = app.listen(process.env.PORT || 3000, () => {
console.log(`Server started (PID: ${process.pid}) on port ${process.env.PORT}`)
})
let isShuttingDown = false
async function gracefulShutdown(signal) {
if (isShuttingDown) return
isShuttingDown = true
console.log(`${signal} received — starting graceful shutdown...`)
// 1. Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed')
try {
// 2. Close database connections
await prisma.$disconnect()
// 3. Close Redis connection
await redis.quit()
console.log('All connections closed — exiting cleanly')
process.exit(0)
} catch (err) {
console.error('Error during shutdown:', err)
process.exit(1)
}
})
// Force exit after 30 seconds (safety net)
setTimeout(() => {
console.error('Forced shutdown after timeout')
process.exit(1)
}, 30_000)
}
// PM2 sends SIGINT for pm2 stop, SIGTERM for pm2 reload
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))Test Your Knowledge
Ready to test what you've learned? Take the quiz with 10 questions and see your score.