PM2 Cluster Mode vs Fork Mode: Choosing the Right Execution Mode for Your Node.js App
Cluster mode scales a stateless Node.js app across CPU cores with zero-downtime reloads; fork mode keeps in-process state safe. A decision table, ecosystem.config.js for both, and validation steps.
25 Aug 2025, 09:14 UTC

Every PM2 deployment eventually hits the same fork in the road: run the app in cluster mode to spread traffic across CPU cores, or stay in fork mode and scale with separate instances behind a load balancer. The wrong choice shows up later as dropped WebSocket connections, duplicated cron jobs, or CPU throttling in containers. The decision is cheap to make correctly up front and expensive to retrofit.
The decision and its constraints
Cluster mode uses Node.js's native cluster module: PM2 spawns one master process plus N worker processes that share a single listening port. Fork mode runs exactly one process per app entry — the classic single-process model.
Cluster mode is only safe when the application is stateless. If your app keeps any of the following in process memory, cluster mode will break it in subtle ways:
- Session state stored in a local object or
Map - WebSocket or Socket.IO connections without a shared adapter (e.g., Redis)
- In-memory caches, rate limiters, or queues
- Scheduled jobs (
node-cron,setInterval) that must run exactly once — with 4 workers, they run 4 times
Fork mode preserves in-process state but caps a single app at one CPU core. Node.js is single-threaded for your JavaScript, so a CPU-bound fork-mode app saturates one core while the rest idle.
Side-by-side comparison
| Concern | Cluster mode | Fork mode |
|---|---|---|
| CPU utilization | One worker per core, shared port | One core per app instance |
| Zero-downtime reload | pm2 reload recycles workers one at a time | Full process restart; brief unavailability unless multiple instances sit behind a load balancer |
| In-memory state | Must be externalized (Redis, DB) | Safe within the single process |
| Memory overhead | Master process (~30–50 MB) plus workers | No master; each instance fully independent |
| Failure isolation | A crashed worker doesn't take down siblings | A crash takes down the whole app |
| Monitoring | pm2 list shows one row per worker; APM tools may need worker correlation config | One row per process; metrics map 1:1 |
| Port binding | Workers must share the port — code must not hardcode listen(3000) per worker expecting exclusivity | Each instance needs its own port if you run several |
Trade-offs worth understanding before you commit
Load distribution is the kernel's job, not PM2's. The operating system hands incoming connections to workers, and under low concurrency the spread can be uneven. Cluster mode is not a smart load balancer.
Graceful shutdown is your responsibility in both modes. PM2 sends SIGINT and waits kill_timeout (default 1600 ms) before force-killing. If your app doesn't close the HTTP server and drain connections on signal, reloads will drop requests regardless of mode.
instances: 'max' is dangerous in containers. It spawns one worker per visible CPU core. In Kubernetes or Docker with CPU limits, the container may see the host's core count, so you spawn 16 workers against a 2-CPU quota and get throttled. Set an explicit integer matching the cgroup limit.
Implementation: both modes in ecosystem.config.js
Run these commands on the server where the app lives, as the user that should own the processes (not root). Save this as ecosystem.config.js in the app directory:
module.exports = {
apps: [
{
name: 'api-cluster',
script: './server.js',
exec_mode: 'cluster',
instances: 2, // explicit integer, not 'max', in containers
instance_var: 'INSTANCE_ID',
kill_timeout: 5000, // give the app 5s to drain connections
env_production: {
NODE_ENV: 'production', // PM2 does not set this automatically
PORT: 3000
}
},
{
name: 'worker-fork',
script: './jobs.js', // cron/queue worker: must run exactly once
exec_mode: 'fork',
instances: 1,
env_production: {
NODE_ENV: 'production'
}
}
]
};
The application must read its port from the environment and handle shutdown signals:
const port = process.env.PORT || 3000;
const server = app.listen(port);
process.on('SIGINT', () => {
server.close(() => process.exit(0)); // stop accepting, drain, exit
});
Start and verify:
pm2 start ecosystem.config.js --env production
pm2 list # expect 2 rows for api-cluster, 1 for worker-fork
pm2 show api-cluster # confirms exec_mode and instance count
Risk note: pm2 start changes running state. If an app with the same name already exists, use pm2 delete <name> first or pm2 startOrReload to avoid duplicate entries.
Validating the choice
Three checks confirm the mode behaves as expected:
- Reload behavior: while running
pm2 reload api-cluster, hammer the endpoint withwrk -t2 -c10 -d15s http://localhost:3000/. Cluster mode should show zero 5xx responses. Repeat against a fork-mode app and you'll see a brief error window — that's the trade-off in action. - Worker isolation: watch
pm2 monitwhile generating load. CPU should spread across both cluster workers. If one worker does all the work at low concurrency, that's the kernel distribution caveat, not a bug. - Container alignment: in Kubernetes, set
instances: 2to match acpu: "2"limit, then checkkubectl top podunder load for throttling symptoms (CPU pinned at the limit with rising latency).
Limitations
This guidance reflects long-standing PM2 5.x behavior; confirm flag defaults against your installed version with pm2 --version and the changelog. Cluster mode does not fix apps that are stateful by design — externalizing sessions, WebSocket adapters, and job schedulers is application work PM2 cannot do for you. And if you already run multiple replicas behind a proper load balancer (Kubernetes Services, nginx), fork mode per replica is often the simpler, more observable choice; cluster mode earns its keep mainly on bare VMs where PM2 is the whole process layer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.