---
title: "How to Choose Between Fork and Cluster Mode in PM2"
description: "PM2 fork vs cluster mode: key differences, when to use each, plus graceful reload, zero-downtime restart, and production best practices for Node.js applications."
date: 2026-07-18
categories: ["tools"]
tags: ["pm2","node","process-management"]
---

import Button from "@components/widgets/Button.astro";
import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";

PM2 fork vs cluster mode is one of the first decisions you face when running Node.js applications in production. [PM2](https://pm2.keymetrics.io/) is the most widely used Node.js process manager (43,000+ GitHub stars, 600M+ npm downloads), and it gives you two fundamentally different ways to run your apps. Pick the wrong mode and you either waste resources or break features like graceful reload.

PM2 v7.0.3 (June 2026) is the current release. It requires Node.js >= 18, adds Bun runtime support, ships OpenTelemetry tracing, and includes native source maps. If you're running an older PM2, the upgrade path matters. More on that below.

This guide covers what each mode does, the real differences (including some corrections to what you'll find elsewhere), when to pick each one, and the production best practices that keep your apps alive.

For a broader PM2 tutorial, see [managing applications with PM2](https://www.bitdoze.com/pm2-manage-apps/).

<Notice type="info" title="Quick Decision: Fork or Cluster?">

| Use Fork Mode When | Use Cluster Mode When |
|---|---|
| Background jobs, cron, workers, scripts | HTTP, TCP, or WebSocket servers |
| Non-Node.js apps (Python, Ruby, binaries) | Node.js-only apps |
| Different Node versions per app | All apps on same Node version |
| Memory-constrained VPS (1-2 GB) | Multi-core VPS (2+ cores, 2+ GB) |
| Development or local testing | Production with uptime requirements |

</Notice>

## What is fork mode in PM2?

Fork mode is PM2's default. It spawns a single process per application using Node's `child_process.fork()`. One process, one app.

Fork mode is the right choice for:

- **Scripts, workers, and cron jobs:** anything that doesn't listen on a port.
- **Non-Node.js runtimes:** fork mode can run Python, Ruby, or arbitrary binaries.
- **Apps needing different Node versions:** use `node_args` to point at a specific binary.
- **Development and testing:** simpler, lower memory footprint.

### Basic fork mode usage

```sh
# Run app.js in fork mode (default)
pm2 start app.js
```

Or with an ecosystem file:

```js
// ecosystem.config.js
module.exports = {
  apps: [{
    script: "app.js",
    exec_mode: "fork"
  }]
};
```

```sh
pm2 start ecosystem.config.js
```

A common misconception: features like cron restarts, source map support, and custom log formats are **not** fork-mode exclusives. These are general PM2 attributes that work in both fork and cluster mode. PM2 v7 replaced the `source-map-support` npm dependency with native `process.setSourceMapsEnabled()`, so source maps work everywhere without extra packages.

For managing [PM2 environment variables](https://www.bitdoze.com/pm2-env-vars/) across modes, see the linked guide.

## What is cluster mode in PM2?

Cluster mode spawns multiple worker processes using the Node.js `cluster` module. Each worker is a separate OS process running your app, and they share the same server port via IPC handle sharing. That's a Node.js feature, not something PM2 invented. The cluster module handles round-robin load balancing across workers.

### Starting cluster mode

```sh
# Run with as many workers as CPU cores
pm2 start app.js -i max

# Run with exactly 4 workers
pm2 start app.js -i 4
```

With an ecosystem file:

```js
// ecosystem.config.js
module.exports = {
  apps: [{
    script: "app.js",
    instances: "max",
    exec_mode: "cluster"
  }]
};
```

Since PM2 v7, setting `instances` on a Node.js app automatically enables cluster mode. Explicit `exec_mode: "cluster"` is optional but recommended for clarity. Your future self will thank you.

The `instances` values:

- `"max"` or `0`: one worker per CPU core
- `-1`: all cores minus one (leaves headroom for the OS)
- Any number: that many workers

### Cluster mode advantages

- **Multi-core utilization:** your app handles parallel requests across all cores.
- **Fault tolerance:** if one worker crashes, PM2 restarts it while the others keep serving.
- **Graceful reload / zero-downtime restart:** `pm2 reload` replaces workers one-by-one (covered in detail below).

### Cluster mode limitations

- **Higher memory usage:** each worker is a separate process. 4 workers at 200 MB each = 800 MB minimum.
- **Requires the same Node.js version** for all workers (cluster module limitation). Bun is supported in PM2 v7+ but has edge cases. Test thoroughly.
- **Requires stateless applications.** This is the gotcha most tutorials skip.

<Notice type="warning" title="Stateless Apps Required">

If your app stores session data in memory, uses in-process caches, or relies on shared mutable state, cluster mode will break it. Worker A gets a request, stores the session. Worker B gets the next request and has no idea about that session.

Use Redis, Memcached, or your database for shared state. For WebSocket apps, see the sticky session section below.

</Notice>

<Notice type="info" title="Memory Math">

4 instances x 200 MB = 800 MB RAM. On a [2 GB Hetzner Cloud VPS](https://go.bitdoze.com/hetzner), you can safely run about 8 instances total across all clustered apps (leaving room for the OS and PM2 itself). Plan your instance count before you hit OOM.

</Notice>

## PM2 fork vs cluster mode: key differences

| Feature | Fork Mode | Cluster Mode |
|---|---|---|
| Process model | Single process per app | Multiple workers per app |
| Load balancing | None (or external) | Automatic (Node.js cluster module) |
| Graceful reload (`pm2 reload`) | Falls back to restart (brief downtime) | True rolling restart (zero downtime) |
| Port sharing | Separate ports required | Same port via IPC handle sharing |
| Non-Node.js runtimes | Supported (Python, Ruby, binaries) | Node.js only (Bun supported in v7+) |
| Multiple Node versions | Supported via `node_args` | Single version (cluster module limitation) |
| Cron restarts | Yes | Yes |
| Source map support | Yes (native in v7+) | Yes (native in v7+) |
| Custom log formats | Yes | Yes |
| Memory per instance | Lower (single process) | Higher (N x single instance) |
| Best for | Workers, scripts, cron, mixed runtimes | HTTP/TCP servers, production web apps |

If you're sizing a VPS for cluster mode, [benchmark your VPS](https://www.bitdoze.com/benchmark-cloud-servers/) first to understand the actual headroom.

Cluster mode is usually the better default for production web apps that serve HTTP traffic. Fork mode wins for non-networked workloads, background jobs, and environments where you need multiple runtimes or Node versions.

## Graceful reload and zero-downtime restart in PM2

Graceful reload and zero-downtime restart are what separate a dev setup from a production deployment. Most PM2 guides skip this part.

### pm2 reload vs pm2 restart

These are different commands with different behavior:

```bash
pm2 reload my-app     # rolling restart (cluster) / restart (fork)
pm2 restart my-app    # immediate kill + start
```

- **`pm2 restart`** kills all processes, then starts them. Brief downtime in fork mode. Full downtime in cluster mode (all workers killed at once).
- **`pm2 reload`** in cluster mode is a true rolling restart. Workers are replaced one-by-one; at least one worker is always serving traffic. In fork mode, `pm2 reload` falls back to `pm2 restart` (downtime), because there's only one process. There's nothing to roll.

For zero-downtime deploys in cluster mode, always use `pm2 reload`.

### Application-side graceful shutdown (SIGINT handling)

Setting `wait_ready: true` in your ecosystem config tells PM2 to wait for a "ready" signal from your app before considering it started. But that signal has to come from your code. When PM2 sends a SIGINT to reload a worker, your app needs to handle it: close the server, flush connections, exit cleanly.

Without this code, PM2 will wait for `listen_timeout`, then force-kill the process. That's not graceful. It's a timeout with data loss risk.

```js
const express = require('express');
const app = express();

// ... your routes and middleware ...

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () => {
  console.log(`Worker ${process.pid} listening on ${PORT}`);
  // Tell PM2 the worker is ready (required with wait_ready: true)
  if (process.send) {
    process.send('ready');
  }
});

// Handle PM2's graceful reload signal
process.on('SIGINT', () => {
  console.log('SIGINT received, closing server...');
  server.close(() => {
    // Close DB connections, flush logs, release resources
    console.log('Server closed, exiting');
    process.exit(0);
  });
  // Safety: force exit if not closed within kill_timeout
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 5000);
});
```

The ecosystem config to pair with this:

```js
module.exports = {
  apps: [{
    name: "my-api",
    script: "./app.js",
    instances: "max",
    exec_mode: "cluster",
    wait_ready: true,        // wait for process.send('ready')
    listen_timeout: 5000,    // ms to wait for 'ready' signal
    kill_timeout: 5000,      // ms to wait for clean exit after SIGINT
  }]
};
```

<Notice type="warning" title="Without This Code, Graceful Reload Won't Work">

If your app doesn't handle SIGINT and call `process.send('ready')`, PM2 will wait for `listen_timeout` then force-kill. Your users get dropped connections. Add the shutdown handler above before enabling `wait_ready`.

</Notice>

## PM2 cluster mode best practices for production

### Production ecosystem configuration

Generate a starter file with `pm2 init simple` (or `pm2 ecosystem`, both still work). Then build it out:

```js
// ecosystem.config.js
module.exports = {
  apps: [{
    name: "my-api",
    script: "./app.js",
    instances: "max",
    exec_mode: "cluster",              // explicit, recommended for clarity
    max_memory_restart: "300M",         // auto-restart if worker exceeds 300 MB
    wait_ready: true,                   // wait for process.send('ready')
    listen_timeout: 5000,               // ms to wait for ready signal
    kill_timeout: 5000,                 // ms to wait for clean shutdown
    max_restarts: 10,                   // restart limit within min_uptime window
    min_uptime: "10s",                  // if process runs less than 10s, it's a crash
    log_date_format: "YYYY-MM-DD HH:mm:ss Z",
    merge_logs: true,                   // merge worker logs into one file
    env: {
      NODE_ENV: "development",
    },
    env_production: {
      NODE_ENV: "production",
    },
  }]
};
```

Start in production:

```bash
pm2 start ecosystem.config.js --env production
```

### Instance scaling strategy

Not every app needs `instances: "max"`. Scale based on your workload:

- **CPU-bound apps:** `instances: "max"` (one per core).
- **I/O-bound apps:** can try 1.5x-2x core count; watch memory.
- **Memory-constrained VPS:** calculate `available_RAM / per_instance_RAM` = safe max.

Dynamic scaling without restart:

```bash
pm2 scale my-app +2     # add 2 more workers
pm2 scale my-app 4      # scale to exactly 4 workers
```

### Environment variable management

A common gotcha: when you restart via CLI with new env vars, they won't update unless you pass `--update-env`:

```bash
NODE_ENV=production pm2 restart my-app --update-env
```

Ecosystem files always update env vars on restart or reload. Use them when possible. See [PM2 environment variables](https://www.bitdoze.com/pm2-env-vars/) for the full details.

### Security: filter_env

If your VPS has other services or users, prevent sensitive env vars from leaking into PM2 child processes with `filter_env`:

```js
// In ecosystem.config.js
module.exports = {
  apps: [{
    name: "my-api",
    script: "./app.js",
    // Only pass these env var prefixes to the worker
    filter_env: ["NODE_", "APP_", "DB_"],
    // Or strip ALL global env vars:
    // filter_env: true,
    // ...
  }]
};
```

For broader VPS hardening, see [securing your VPS server](https://www.bitdoze.com/crowdsec-secure-server/).

### Log rotation

By default, PM2 logs grow until your disk is full. Install the log rotation module:

```bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:compress true
```

This keeps logs to 10 MB per file, retains 30 rotated files, and compresses old ones. Essential for any production VPS.

For broader monitoring, see [monitoring CPU usage on Linux](https://www.bitdoze.com/monitor-cpu-usage-and-send-email-alerts-in-linux/).

<ListCheck>

**Production checklist**

- Set `max_memory_restart` to prevent memory leaks from crashing the box
- Set `wait_ready: true` and implement SIGINT handling in your app
- Set `max_restarts` and `min_uptime` to catch crash loops
- Use `merge_logs: true` to keep log files manageable
- Install `pm2-logrotate` to prevent disk fill
- Use ecosystem files (not CLI flags) for reproducible deploys
- Run `pm2 save` and `pm2 startup` to survive reboots
- Keep Node.js and dependencies updated. See [keeping your Node.js dependencies updated](https://www.bitdoze.com/nodejs-update-dependencies/)

</ListCheck>

## PM2 in containers: pm2-runtime for Docker

If you're running PM2 inside a Docker container, use `pm2-runtime` instead of `pm2 start`.

Regular `pm2 start` spawns a daemon process, wrong for containers. The container starts, PM2 forks, the main process exits, and Docker thinks the container is done. `pm2-runtime` stays in the foreground and streams logs to stdout/stderr, which is what Docker expects.

```dockerfile
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .

CMD ["pm2-runtime", "start", "ecosystem.config.js"]
```

Or without an ecosystem file:

```dockerfile
CMD ["pm2-runtime", "npm", "--", "start"]
```

One decision: in Docker, cluster mode scales via `instances` in the ecosystem file **or** via Docker/Kubernetes replicas. Pick one, don't do both, or you'll get N x M workers and wonder why your container is OOM-killed.

For more on container commands, see [Docker commands for production](https://www.bitdoze.com/docker-commands/). Fork mode in PM2 can also run [Python apps in Docker](https://www.bitdoze.com/docker-run-python/) or other non-Node.js runtimes.

## WebSocket and sticky session considerations

Cluster mode uses round-robin load balancing by default. For HTTP requests, that's fine. Each request is independent. For WebSocket connections, it's a problem. The initial handshake lands on Worker A, but subsequent frames might get routed to Worker B, which has no idea about that connection.

Three solutions:

1. **Sticky sessions at the reverse proxy:** Nginx: `ip_hash`. HAProxy: `cookie`-based stickiness. This pins a client to one worker.
2. **Redis adapter for socket.io:** `@socket.io/redis-adapter` lets workers share connection state. Works well but adds Redis as a dependency.
3. **Separate WebSocket server:** run your WebSocket handler in fork mode on a different port, alongside your clustered HTTP server. Simple, no shared state needed.

Pick the option that matches your architecture. If you're running a single VPS with Nginx in front, sticky sessions are the easiest path.

## PM2 v7: what's new (2025-2026)

PM2 v7.0.0 (May 2026) was a major release. Key changes relevant to the fork vs cluster decision:

- **Node.js >= 18 required.** PM2 dropped Node 16 support. If you're still on 16, you need to upgrade first.
- **Bun runtime support.** Fork mode since v6.0.5, cluster mode since v7.0.0. Caveat: Bun + cluster has edge cases (see GitHub issues). Test thoroughly before deploying.
- **Native source maps.** Replaced the `source-map-support` npm dependency with `process.setSourceMapsEnabled()`. Works in both modes, no extra packages needed.
- **OpenTelemetry built-in.** `@opentelemetry/api`, `sdk-node`, and `auto-instrumentations-node` are direct dependencies. Caveat: in cluster mode, workers may report duplicate spans (known OpenTelemetry issue).
- **`pm2 ls` shows host metrics by default** (v7.0.2). CPU, memory, disk at a glance.
- **`max_memory_restart` shown in `pm2 describe`** (v7.0.1). Easier to audit memory limits.
- **Security fixes.** CVE-2025-5891 (ReDoS), CVE-2026-27699 (proxy-agent), three command injection fixes, prototype pollution fix. If you're on v5 or v6, these are good reasons to upgrade.

<Notice type="info" title="Upgrading from PM2 v5/v6?">

PM2 v7 requires Node.js >= 18. Check your Node version first (`node -v`). The security fixes alone justify the upgrade. Run `npm install -g pm2@latest` then `pm2 update` to reload your processes with the new PM2 binary. If you're using Bun, see [updating packages with Bun](https://www.bitdoze.com/bun-update-packages/) for the upgrade workflow.

</Notice>

## Troubleshooting PM2 fork and cluster issues

<Accordion label="EADDRINUSE: port already in use" group="faq">

In fork mode, two apps can't share a port. If you see `EADDRINUSE`, another process (or another PM2 app) is already listening on that port.

```bash
# Check what's using the port
lsof -i :3000

# Check PM2 apps for conflicts
pm2 list
```

In cluster mode, workers share the port automatically. You won't hit this within one app. But two different cluster-mode apps still can't share a port.

</Accordion>

<Accordion label="Workers crashing in a loop" group="faq">

If you see `pm2 restart count exceeded` in the logs, your workers are crashing faster than `min_uptime` allows. PM2 stops restarting after `max_restarts` attempts.

```bash
# Check logs for the root cause
pm2 logs my-app --lines 100

# Describe the app for restart counts
pm2 describe my-app
```

Fix the underlying bug first. If it's a transient issue (e.g., database not ready), increase `max_restarts` or lower `min_uptime` temporarily.

</Accordion>

<Accordion label="Graceful reload not working" group="faq">

If `pm2 reload` kills connections instead of draining them, your app probably isn't handling SIGINT or isn't sending `process.send('ready')`. See the graceful shutdown code in the best practices section above.

Verify:

```bash
# Check if wait_ready is enabled
pm2 describe my-app | grep wait_ready

# Check logs for "SIGINT received" message during reload
pm2 logs my-app
```

If you don't see "SIGINT received" in the logs during reload, your handler isn't registered.

</Accordion>

<Accordion label="Memory leaks / OOM" group="faq">

Use `max_memory_restart` to auto-restart workers that exceed a memory threshold. This catches leaks before they crash the whole VPS.

```bash
# Watch memory in real time
pm2 monit
```

If a specific worker keeps hitting the limit, it's leaking. Fix the code. `max_memory_restart` is a band-aid, not a cure.

</Accordion>

<Accordion label="WebSocket connections dropping in cluster mode" group="faq">

PM2's cluster mode uses round-robin by default. WebSocket connections that start on one worker may get routed to another on reconnect. See the sticky session section above for solutions: Nginx `ip_hash`, Redis adapter, or a separate fork-mode WebSocket server.

</Accordion>

<Accordion label="Environment variables not updating" group="faq">

When restarting via CLI with new env vars, you must pass `--update-env`:

```bash
NODE_ENV=production pm2 restart my-app --update-env
```

Ecosystem files always update env vars on restart/reload. This is the most common PM2 gotcha in CI/CD pipelines. Your deploy script sets a new env var, restarts, and nothing changes.

</Accordion>

### Quick verification commands

After deploying, confirm everything is running as expected:

```bash
pm2 list          # check mode column (fork/cluster), instance count, status
pm2 describe 0    # detailed info: exec_mode, restart count, memory limits
pm2 monit         # live CPU/memory per instance
pm2 logs          # check for startup errors
```

## Conclusion

The old "fork for features, cluster for scaling" split is outdated. Cron restarts, source maps, and custom log formats work in both modes. Choose based on your app's architecture:

- **Cluster mode** for production HTTP/TCP servers that need uptime and multi-core utilization. The zero-downtime reload alone makes it worth the extra memory.
- **Fork mode** for background workers, scripts, cron jobs, non-Node.js runtimes, and environments where you need different Node versions per app.

If you're running on a budget VPS like [Hetzner Cloud](https://go.bitdoze.com/hetzner) or [Hostinger VPS](https://go.bitdoze.com/hostinger-vps), cluster mode with 2 to 4 instances works fine on a 2 to 4 GB plan. Just do the memory math first.

For the full PM2 tutorial covering process management, monitoring, and deployment, see [managing applications with PM2](https://www.bitdoze.com/pm2-manage-apps/). If you're looking at broader server management options, check out [self-hosted server panels](https://www.bitdoze.com/best-self-hosted-panels/) for alternatives to manual PM2 management.

<Button text="PM2 Management Tutorial" link="https://www.bitdoze.com/pm2-manage-apps/" variant="solid" color="blue" size="md" icon="arrow-right" />