Choosing Between PM2 restart, reload, and gracefulReload for Node.js Deployments
A decision guide for PM2 zero-downtime deployments: compare restart, reload, and gracefulReload, understand cluster-mode constraints, and validate connection draining under load.
30 May 2026, 23:31 UTC

The deployment decision you're actually making
When you push a Node.js update with PM2, the command you run determines whether users see dropped connections, 502 errors, or nothing at all. The choice is not about which command is newest; it is about matching the command to your process mode, your application's shutdown behavior, and your hosting topology.
Constraints that eliminate options before you compare them
Before weighing commands, verify these constraints. If any fail, some options are off the table entirely.
- Cluster mode is mandatory for zero-downtime. PM2's rolling restart only works when
instances > 1andexec_mode: 'cluster'. In fork mode (instances: 1),reloadandgracefulReloadbehave like a hardrestartbecause there is no second worker to keep the port alive. - Your application must handle SIGINT. Node.js exits immediately on SIGINT by default. Without an explicit handler that closes the server and waits for in-flight requests, PM2's graceful timeout does nothing useful.
- PM2 zero-downtime is single-host only. If you run multiple hosts behind a load balancer, PM2 coordinates workers on one machine. Cross-host draining requires external orchestration.
- Container platforms should usually own the rollout. In Kubernetes or Docker Swarm, run PM2 with
instances: 1and let readiness/liveness probes manage availability. PM2's internal rolling update fights the orchestrator's rolling update.
Comparing the three supported commands
Assuming cluster mode with at least two instances, PM2 exposes three distinct behaviors:
| Command | Connection handling | Typical downtime | Requires SIGINT handler | Best for |
|---|---|---|---|---|
pm2 restart | Drops in-flight requests immediately | ~100–500 ms | No | Config-only changes, development, fork-mode apps |
pm2 reload | SIGINT, then SIGKILL after default timeout (~1.6 s) | Zero for stateless HTTP | Yes, for draining | Fast rolling updates of stateless HTTP services |
pm2 gracefulReload | SIGINT with configurable drain window (gracefulReloadTimeout) | Zero with draining | Yes | Long requests, file uploads, WebSocket-adjacent workloads |
The practical difference between reload and gracefulReload is the timeout budget and intent. reload recycles workers quickly; gracefulReload gives your code time to finish work before the process is killed.
Implementing gracefulReload for a stateful HTTP worker
The following configuration assumes PM2 5.x or later, which supports wait_ready and listen_timeout. Save it as ecosystem.config.js:
module.exports = {
apps: [{
name: 'api',
script: 'server.js',
instances: 4,
exec_mode: 'cluster',
gracefulReloadTimeout: 10000,
listen_timeout: 5000,
wait_ready: true,
env_production: {
NODE_ENV: 'production'
}
}]
};
Key fields:
gracefulReloadTimeout: 10000— PM2 waits up to 10 seconds after SIGINT before sending SIGKILL. Raise this if requests routinely take longer than a few seconds.listen_timeout: 5000— PM2 waits up to 5 seconds for the new worker to bind its port before considering it ready. Prevents traffic from hitting a worker that is still booting.wait_ready: true— The worker must callprocess.send('ready')before PM2 routes traffic. Useful for non-HTTP workers or apps that connect to a database before serving.
Application shutdown handler
Graceful reload is useless if your process ignores SIGINT. Add this to your entry point:
const server = require('./app').listen(3000);
if (process.send) {
process.send('ready');
}
process.on('SIGINT', () => {
server.close(() => {
process.exit(0);
});
// Force exit if draining exceeds the PM2 timeout
setTimeout(() => {
process.exit(1);
}, 8000).unref();
});
The handler stops the listener, waits for active connections to finish, then exits cleanly. The setTimeout is a safety valve: if a socket hangs, the process still exits before PM2 escalates to SIGKILL.
Validating under sustained load
Never assume graceful reload works. Validate it with a load generator while the reload runs.
- Start the application in production mode:
pm2 start ecosystem.config.js --env production - In a second terminal, generate sustained traffic. Using
hey:hey -z 30s -c 50 http://localhost:3000/ - While load is running, trigger the reload in a third terminal:
pm2 gracefulReload api - Inspect the
heysummary. A successful graceful reload shows zero failed requests and no connection resets. - Check PM2 logs for sequential worker recycling:
pm2 logs --lines 100
Run a negative control: remove the SIGINT handler, repeat the load test, and observe connection resets or 502 errors. If the negative test does not fail, your load generator is not exercising the path you think it is.
When gracefulReload is the wrong tool
gracefulReload recycles workers using the same ecosystem configuration. If you changed environment variables, the script path, or instance count in ecosystem.config.js, gracefulReload will not apply those changes. Use pm2 reload or pm2 restart after editing the ecosystem file so PM2 re-reads the process definition.
WebSocket connections deserve special attention. Node's server.close() stops new HTTP connections but does not automatically close WebSocket upgrades. If your app maintains long-lived WebSockets, add explicit logic to send a close frame and terminate the socket within your drain window. Otherwise the worker will hang until gracefulReloadTimeout expires and PM2 kills it.
Rollback and failure handling
If validation fails or a bad release reaches production, redeploy the previous version using the same pm2 gracefulReload procedure rather than falling back to pm2 restart. Mixing a hard restart into a graceful pipeline reintroduces the downtime you were trying to avoid. PM2 does not version your release artifacts; your rollback strategy depends on redeploying a known-good code path through the same graceful mechanism.
Limitations to plan around
- Timeout calibration is load-dependent. A 10-second drain window is generous for a health-check endpoint and useless for a 60-second report export. Measure your p99 request duration under production traffic and set
gracefulReloadTimeoutabove it, with headroom. - Multi-host deployments need more than PM2. PM2 drains connections on one host. If you run three app servers behind HAProxy or Nginx, you must coordinate host removal at the load balancer before running
gracefulReloadon each node. - Fork mode has no zero-downtime path. If your application cannot run in cluster mode (for example, it relies on a single global state that cannot be shared), plan for brief downtime with
pm2 restartor move to an orchestrator-managed rollout.
Practical verification checklist
Before trusting this in production, confirm:
- PM2 version is 5.x or later:
pm2 --version pm2 listshowsclustermode and your target instance count- The SIGINT handler appears in your logs during a test reload
- Load-test output shows zero failures across at least one full reload cycle
Zero-downtime deployment with PM2 is achievable, but it is a property of your whole system—process mode, shutdown code, timeout budget, and hosting topology—not a flag you flip.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.