TL;DR
How to achieve zero-downtime deployments for Node.js using PM2 cluster mode with graceful reload and health verification.
Key facts
- Topology
- Node.js + PM2 cluster
TL;DR
Zero-downtime deployment for Node.js means deploying new code without dropping a single request. This requires PM2's cluster mode, a graceful reload strategy, and health verification between steps.
How PM2 cluster reload works
When you run pm2 reload, PM2:
- Starts a new worker with the updated code
- Waits for it to signal "ready"
- Sends the old worker a SIGINT to begin graceful shutdown
- The old worker stops accepting new connections and drains in-flight requests
- Repeats for each worker in the cluster
This rolling reload means at least one worker is always handling traffic.
Application requirements
Your app must signal readiness to PM2 and handle graceful shutdown:
const server = app.listen(PORT, () => {
if (process.send) {
process.send('ready');
}
});
process.on('SIGINT', () => {
server.close(() => {
process.exit(0);
});
});
Enable wait-ready in your ecosystem config:
module.exports = {
apps: [{
name: 'api',
script: 'server.js',
instances: 'max',
exec_mode: 'cluster',
wait_ready: true,
listen_timeout: 10000,
kill_timeout: 5000,
}],
};
Deployment script
A safe deployment sequence:
#!/bin/bash
set -euo pipefail
APP_DIR="/var/www/myapp"
cd "$APP_DIR"
git fetch origin main
git reset --hard origin/main
npm ci --production
npm run build
pm2 reload ecosystem.config.js --env production
sleep 3
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$HTTP_CODE" != "200" ]; then
echo "Health check failed — rolling back"
git reset --hard HEAD~1
npm ci --production
pm2 reload ecosystem.config.js --env production
exit 1
fi
echo "Deployment complete"
nginx considerations
If nginx is caching upstream connections with keepalive, the reload is seamless. Ensure proxy_http_version 1.1 and Connection "" headers are set so nginx reuses connections to the new workers.
Common pitfalls
- Missing
wait_ready: PM2 sends traffic to the new worker before it finishes initialising - Missing SIGINT handler: The old worker is killed abruptly, dropping in-flight requests
- Database migrations: Run migrations before reloading workers, and ensure they are backward-compatible with the current running code
With Reflex
Reflex orchestrates zero-downtime deploys end-to-end: it pulls your code, runs the build, executes a rolling PM2 reload, runs health checks, and automatically rolls back if the health check fails — all without SSH access or manual scripting. See How it works.