Zero-Downtime Reloads With PM2 Cluster Mode: What Actually Has to Be True
PM2 can reload Node.js workers without dropping requests, but only if the app is stateless and handles shutdown signals. Here is the practical checklist.
06 Oct 2025, 10:07 UTC

The deploy problem: a restart is user-visible
A plain Node.js restart has a dead window. The old process stops accepting connections, in-flight requests can be cut off, and anything that arrives while the new process is booting fails. If you do that by hand during a deploy, users see it.
The useful takeaway: PM2 cluster mode can make HTTP reloads nearly invisible for stateless apps because it runs multiple workers and replaces them one at a time. It is not automatic protection. The app still has to shut down cleanly, and any in-memory state has to move elsewhere.
What cluster mode changes
When you start an app with pm2 start app.js -i max, PM2 runs it in cluster mode instead of fork mode. PM2 creates a small master process and several worker processes. The workers share the same server port, and Node's cluster module distributes accepted connections between them.
That matters for reloads: PM2 can retire one worker while the others keep serving. If your app is stateless and handles shutdown signals, a reload becomes a rolling replacement inside one machine.
| Mode | Good fit | Main trade-off |
|---|---|---|
| Fork mode | One process, simple scripts, jobs where in-memory state is acceptable | No built-in multi-worker load balancing; restart has a real gap |
| Cluster mode | Stateless HTTP services that need rolling reloads | More memory, more processes to observe, state must be external |
Cluster mode is still one host
PM2 cluster mode balances across CPU workers on the same machine. It does not replace a load balancer across hosts. If you run only one worker, a reload can still have a gap. Use at least two workers or put PM2 behind an external load balancer if you need stronger availability.
Graceful shutdown is the part you own
PM2's reload path signals a worker to stop, waits for it to exit, then starts the replacement. In recent PM2 versions this is commonly a SIGINT; some paths may use SIGTERM. Handle both.
Do not rely on Node's beforeExit event for deploy shutdown. beforeExit is not a signal handler; it can fire when the event loop empties and it does not give you a reliable place to stop accepting work. Use signal handlers instead.
A minimal shutdown handler should: stop accepting new connections, finish in-flight responses, close database or queue connections, then exit. If the process does not exit in time, PM2 force-kills it after the configured kill timeout, and in-flight requests can fail.
Worked example: cluster reload under load
Run these commands in your project directory. Installing PM2 globally may require elevated npm permissions; if you cannot install globally, use a local PM2 install or npx pm2.
npm init -y
npm install express
npm install -g pm2
pm2 start app.js -i max --name my-app
pm2 list
Expected check: pm2 list should show my-app in cluster mode with an instance count matching your CPU core limit. If the container is constrained, replace max with a fixed number such as -i 2.
Use a minimal app that exposes a fast health endpoint and a slow endpoint so you can see both cases:
const express = require('express');
const app = express();
let shuttingDown = false;
app.get('/health', (req, res) => res.status(200).send('ok'));
app.get('/work', async (req, res) => {
await new Promise((resolve) => setTimeout(resolve, 3000));
if (shuttingDown) return res.status(503).send('shutting down');
res.send('done');
});
const server = app.listen(3000, () => console.log('listening'));
function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10000).unref();
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
Now generate steady traffic while you reload. Use three terminals:
# terminal 1
pm2 logs my-app
# terminal 2
hey -z 30s -c 10 http://localhost:3000/health
# terminal 3, while terminal 2 is running
pm2 reload my-app
Expected check: the load tool should not report connection refused or HTTP 5xx for /health. If it does, inspect pm2 logs my-app --err and the app's shutdown logs. The likely causes are an app that ignores signals, a kill timeout that is too short, or only one worker running.
If you point the load tool at /work, a clean shutdown may return an explicit 503 from your handler rather than a dropped socket. That is a design choice: fail fast and let the client retry, or make the shutdown timeout longer than the slowest request.
Set a timeout that matches your slowest request
PM2 lets you bound how long it waits before force-killing a worker. The exact flag name and default vary by version, so verify with pm2 --version and pm2 start --help before relying on it. A conservative pattern is to set the timeout longer than your slowest expected request, then confirm that workers still exit promptly.
Limitations you should decide on before deploying
- Memory overhead: every worker is a separate Node process with its own heap.
-i maxis not free on small containers. - Sticky sessions: PM2 cluster mode does not provide sticky routing by default. If sessions live in process memory, the same client can hit different workers. Use an external store such as Redis, or put a sticky-aware load balancer in front.
- Long-running work: a request that takes 60 seconds needs a shutdown timeout above 60 seconds or it will be killed mid-flight.
- Reload is not migration:
pm2 reloadreplaces code. It does not make database schema changes safe across old and new workers. - Single-worker risk: with one worker, cluster mode still has a restart gap for that worker.
Practical checklist
- Make the HTTP path stateless or move session/state data to an external store.
- Add
SIGINTandSIGTERMhandlers that close the server and finish in-flight work. - Start with
pm2 start app.js -i 2 --name my-app, then scale to-i maxafter checking memory. - Load-test a reload with a fast endpoint and confirm zero connection errors.
- Repeat with a slow endpoint and decide whether retries or longer shutdown timeouts are acceptable.
- For repeatable deploys, move
exec_mode: 'cluster'andinstancesinto an ecosystem file and use PM2's reload-oriented start command for your version.
Cluster mode does not remove deployment risk. It moves the risk from the process boundary into your application's shutdown behavior and state design. Handle those two, and a reload becomes boring, which is the goal.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.