| |

Node.js 66 🟢 Production Process Management and Monitoring with PM2

⏱️ Estimated read: 14 min | |

Running a Node.js process in production with node app.js is a single point of failure. If the process crashes, it stays down. If it consumes too much memory, it takes the server with it. If you need to deploy a new version, you stop the old process and start the new one—creating a window where requests fail. PM2 is the process manager that fills these gaps. It keeps the process alive, restarts it on failure, manages logs, and—critically for production—runs multiple instances of your application across all available CPU cores using Node.js cluster mode .

The cluster mode is PM2’s most consequential feature. A single Node.js process uses one CPU core. On a server with eight cores, seven sit idle. PM2’s cluster mode starts one instance per core, and the instances share the same server port through the underlying Node.js cluster module . This is not a cosmetic improvement. It multiplies the application’s throughput and ensures that a crash in one instance does not take down the entire service—the other instances continue handling requests while PM2 restarts the failed one. The cost is that the application must be stateless. Session data, WebSocket connections, and any other per-process state must move to a shared store like Redis, or the instances will behave inconsistently .

This chapter covers the ecosystem configuration file, cluster mode, zero-downtime reload, graceful shutdown, monitoring commands, and the production configuration settings that prevent PM2 from giving up on a crashing application.

Key point: PM2’s cluster mode runs one instance per CPU core and shares the server port across them; pm2 reload achieves zero-downtime deployment by restarting instances one at a time, and ecosystem.config.js centralizes the configuration for every production setting from instance count to graceful shutdown timeout.


Why PM2 is the production standard

The crash problem. A Node.js process that throws an uncaught exception exits. Without a supervisor, it stays exited. PM2 monitors every managed process and restarts it automatically when it exits . The autorestart option defaults to true, which means a crashed process is brought back without operator intervention. The max_memory_restart option adds a second safety net: if a process exceeds a memory threshold—often a symptom of a leak—PM2 restarts it before the operating system’s OOM killer intervenes .

The core utilization problem. Node.js is single-threaded for JavaScript execution. A server with multiple cores running a single Node.js process wastes the majority of its hardware. Cluster mode solves this by forking one process per core, with the operating system distributing incoming connections across them . The -i max flag auto-detects the core count and starts that many instances. A negative number starts all cores minus that number, which is useful when the server also runs other services .

The deployment problem. pm2 restart kills every instance and starts them again. For the duration of the restart—usually a few seconds—the application is unavailable. pm2 reload restarts instances one by one, always keeping at least one running. The load balancer or the cluster module’s round-robin distribution sends new requests to the surviving instances while the restarted instance initializes . This is zero-downtime deployment, and it requires that the application shuts down gracefully—finishing in-flight requests and closing database connections before exiting.

The observability problem. pm2 status and pm2 monit show CPU usage, memory consumption, restart counts, and uptime for every instance . pm2 logs streams stdout and stderr, with options for filtering, line counts, and raw output . These are not as detailed as a dedicated APM tool, but they are available immediately, require no additional infrastructure, and are often sufficient for diagnosing a production issue.

The configuration drift problem. Starting PM2 with command-line flags means the configuration exists only in the shell history. ecosystem.config.js makes the configuration declarative and version-controlled. It defines every application, its script, its instance count, its environment variables, its log paths, and its restart behavior in a single file that can be committed alongside the code . pm2 start ecosystem.config.js --env production applies the production environment variables defined in the file .


a. The ecosystem configuration file

The ecosystem file is a JavaScript module that exports an object with an apps array. Each entry describes one application. PM2 recognizes files ending in .config.js .

module.exports = {
  apps: [{
    name: 'api',
    script: './dist/server.js',
    cwd: '/var/www/api',
    instances: 'max',
    exec_mode: 'cluster',
    autorestart: true,
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 8080
    },
    error_file: './logs/error.log',
    out_file: './logs/out.log',
    merge_logs: true,
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
  }]
}

The instances option set to 'max' enables cluster mode automatically. The exec_mode option makes it explicit, which is recommended for clarity . The env and env_production blocks define environment-specific variables. Starting with --env production applies the env_production values .

The cwd option is critical when the application is not launched from its own directory. Without it, relative paths for configuration files, assets, and .env files resolve against the wrong directory, and the application fails in ways that do not occur when running node server.js manually .

The max_memory_restart option accepts human-readable sizes: '1G', '500M', '100K' . When an instance exceeds the threshold, PM2 restarts it. This is not a fix for memory leaks—it is a containment mechanism that prevents one leaking instance from exhausting the server’s memory and triggering the OOM killer.

The merge_logs option in cluster mode combines the logs from all instances into a single file rather than creating one file per instance. This makes pm2 logs and log analysis simpler when running multiple instances .


b. Cluster mode and zero-downtime reload

Cluster mode is enabled by the instances option. pm2 start app.js -i max starts one instance per CPU core. In the ecosystem file, the equivalent is instances: 'max' .

The application must be stateless for cluster mode to work correctly. If one instance stores session data in memory and the next request is routed to a different instance, the user appears logged out. WebSocket connections have the same problem: the connection is established with one instance, and if that instance restarts during a reload, the connection drops. The solution is to externalize state—sessions in Redis, WebSocket messages through a pub/sub system, file uploads to object storage rather than local disk .

The reload command is the deployment mechanism. pm2 reload api restarts the instances one at a time. PM2 sends SIGINT to the first instance, waits for it to exit gracefully, starts a replacement, and then moves to the next instance . If an instance does not exit within the kill_timeout window (default 1600ms), PM2 sends SIGKILL and moves on. If the reload process times out entirely, PM2 falls back to a classic restart .

For graceful shutdown, the application must handle SIGINT. This is the signal PM2 sends first during a reload. The handler should stop accepting new connections, finish in-flight requests, close database connections, and then exit:

process.on('SIGINT', async () => {
  server.close(async () => {
    await db.end();
    process.exit(0);
  });
});

The kill_timeout option in the ecosystem file controls how long PM2 waits after SIGINT before sending SIGKILL. If the graceful shutdown takes longer than the default 1600ms—for example, waiting for a slow database query to complete—increase the timeout .


c. Monitoring and production hardening

PM2’s monitoring commands cover the essentials. pm2 status shows a table of every application with its instance count, status, CPU percentage, memory usage, and restart count . pm2 monit opens a real-time dashboard that updates CPU and memory usage and streams logs from the selected process . pm2 logs api streams the logs for a specific application, with --lines to show recent history and --raw for unformatted output .

The restart behavior needs tuning for production. PM2 has a built-in “unstable restart” detection: if a process crashes within min_uptime (default 1000ms) more than max_restarts times (default 16), PM2 stops restarting it . This prevents a tight crash loop from consuming resources indefinitely. However, in some cases it is too aggressive—a process that crashes during initialization due to a missing dependency will hit the limit and stop, leaving the application down. The unstable_restarts option overrides this behavior:

unstable_restarts: 10000

This tells PM2 to keep restarting even during rapid crash cycles, which is appropriate when the crash is caused by a transient condition like a database connection failure that will eventually recover .

The restart_delay option adds a pause before each restart. restart_delay: 4000 waits four seconds, giving the process time to initialize before another restart is attempted. This is useful when the application crashes immediately on startup and restarting in a tight loop would flood the logs without solving the problem .

For containerized deployments, pm2-runtime replaces pm2 start. It keeps the PM2 process in the foreground, which is required by container platforms that expect the main process not to daemonize. CMD ["pm2-runtime", "ecosystem.config.js"] is the standard pattern for Node.js applications in Docker .


Complete Example Session

The following session creates an ecosystem configuration for a production API, starts it in cluster mode, performs a zero-downtime reload, and inspects the monitoring output.

// ============================================
// PART 1: THE ECOSYSTEM FILE
// ============================================
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'api',
    script: './dist/server.js',
    cwd: '/var/www/api',
    instances: 'max',
    exec_mode: 'cluster',
    autorestart: true,
    max_memory_restart: '512M',
    unstable_restarts: 10000,
    restart_delay: 2000,
    kill_timeout: 5000,
    env_production: {
      NODE_ENV: 'production',
      PORT: 8080
    },
    error_file: './logs/error.log',
    out_file: './logs/out.log',
    merge_logs: true,
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
  }]
}
// ============================================
// PART 2: GRACEFUL SHUTDOWN HANDLER
// ============================================
// In the application code (dist/server.js)
const server = app.listen(process.env.PORT, () => {
  console.log(`Worker ${process.pid} listening on ${process.env.PORT}`);
});

process.on('SIGINT', () => {
  console.log(`Worker ${process.pid} shutting down`);
  server.close(() => {
    // Close database connections, clear queues, etc.
    process.exit(0);
  });
});
# ============================================
# PART 3: START IN CLUSTER MODE
# ============================================
pm2 start ecosystem.config.js --env production

# Output shows one instance per CPU core
# Each instance is in "online" status
# ============================================
# PART 4: VERIFY THE CLUSTER
# ============================================
pm2 status

# Output:
# ┌────┬──────┬─────────┬─────────┬───────┬────────┐
# │ id │ name │ mode    │ status  │ cpu   │ mem    │
# ├────┼──────┼─────────┼─────────┼───────┼────────┤
# │ 0  │ api  │ cluster │ online  │ 0%    │ 45mb   │
# │ 1  │ api  │ cluster │ online  │ 0%    │ 42mb   │
# │ 2  │ api  │ cluster │ online  │ 0%    │ 44mb   │
# │ 3  │ api  │ cluster │ online  │ 0%    │ 43mb   │
# └────┴──────┴─────────┴─────────┴───────┴────────┘
# ============================================
# PART 5: ZERO-DOWNTIME RELOAD
# ============================================
# After deploying a new version, reload
pm2 reload api

# Output:
# [PM2] Applying action reloadProcessId on app [api](ids: [ 0,1,2,3 ])
# [PM2] [api](0) ✓
# [PM2] [api](1) ✓
# [PM2] [api](2) ✓
# [PM2] [api](3) ✓

# During the reload, at least one instance was always running
# No requests were dropped
# ============================================
# PART 6: MONITORING
# ============================================
pm2 monit
# Opens a real-time dashboard showing CPU, memory, and logs
# Press Ctrl+C to exit

pm2 logs api --lines 50
# Shows the last 50 lines of logs from all instances
# ============================================
# PART 7: PRODUCTION STARTUP
# ============================================
pm2 startup
# Generates a systemd unit or init script to start PM2 on boot

pm2 save
# Saves the current process list so it is restored on boot

The seven parts cover the ecosystem configuration with production hardening settings, the graceful shutdown handler in the application, starting in cluster mode, verifying the cluster with pm2 status, zero-downtime reload, monitoring with pm2 monit and pm2 logs, and configuring startup persistence. What the session demonstrates is that PM2’s production value comes from the configuration: the instance count, the reload behavior, the memory limit, and the graceful shutdown handler all work together to keep the application available during deployments and failures.


Quick Reference

Core Commands

CommandPurpose
pm2 start ecosystem.config.jsStart all apps in config
pm2 start app.js -i maxStart in cluster mode on all cores
pm2 statusShow process table
pm2 monitReal-time monitoring dashboard
pm2 logs <app>Stream logs
pm2 reload <app>Zero-downtime restart
pm2 restart <app>Hard restart with downtime
pm2 delete <app>Remove from PM2
pm2 startupConfigure boot startup
pm2 savePersist current process list

Ecosystem File Options

OptionTypePurpose
namestringApplication name
scriptstringPath to entry file
cwdstringWorking directory
instancesnumber or 'max'Cluster instance count
exec_mode'cluster' or 'fork'Execution mode
autorestartbooleanRestart on crash (default true)
max_memory_restartstringMemory threshold for restart
kill_timeoutnumberms before SIGKILL after SIGINT
unstable_restartsnumberOverride crash loop detection
restart_delaynumberms between restarts
merge_logsbooleanCombine logs from all instances

Cluster vs Fork Mode

ModeBehaviorUse Case
forkSingle process, no port sharingDevelopment, non-network apps
clusterMultiple processes, shared portProduction HTTP/TCP servers

Reload vs Restart

CommandBehaviorDowntime
pm2 reloadRestarts instances one by oneNone
pm2 restartKills and restarts allBrief

Best Practices

✅ Do This:

instances: 'max', exec_mode: 'cluster'                    // ✅ utilize all cores
max_memory_restart: '1G'                                  // ✅ contain leaks
process.on('SIGINT', () => server.close(() => process.exit(0))) // ✅ graceful
unstable_restarts: 10000                                  // ✅ survive crash loops
pm2 reload api                                            // ✅ zero-downtime deploy

❌ Don’t Do This:

// store sessions in process memory                       // ❌ breaks cluster mode
// kill_timeout: 100 with slow DB shutdown                // ❌ forced SIGKILL
// pm2 restart in production deploy                       // ❌ causes downtime
// no max_memory_restart                                 // ❌ OOM kills server

Common Pitfalls

PitfallWhy It HappensFix
Sessions lost after reloadIn-memory session storeUse Redis or database
Process stops restartingunstable_restarts default too lowSet to 10000
App works manually but not in PM2Wrong cwd or env varsSet cwd and use env_production
Reload hangs then restartsGraceful shutdown too slowIncrease kill_timeout
Memory grows until OOMNo max_memory_restartSet a threshold
PM2 exits immediately in DockerUsed pm2 startUse pm2-runtime

Real-World Examples

1. Cluster mode start

pm2 start app.js -i max

2. Production ecosystem

instances: 'max', exec_mode: 'cluster', env_production: { NODE_ENV: 'production' }

3. Zero-downtime reload

pm2 reload api

4. Memory limit

max_memory_restart: '1G'

5. Graceful shutdown

process.on('SIGINT', () => server.close(() => process.exit(0)));

6. Monitoring

pm2 monit

7. Logs

pm2 logs api --lines 100

8. Startup persistence

pm2 startup && pm2 save

9. Docker

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

10. Crash loop survival

unstable_restarts: 10000, restart_delay: 4000

Visual

Cluster Mode Architecture

┌──────────────────────────────────────────────┐
│  PM2 Master                                  │
│  (shared port, load balancer)                │
│         │                                    │
│    ┌────┼────┬────┐                          │
│    │    │    │    │                          │
│    ▼    ▼    ▼    ▼                          │
│  ┌───┐┌───┐┌───┐┌───┐                        │
│  │W1 ││W2 ││W3 ││W4 │  one per CPU core     │
│  └───┘└───┘└───┘└───┘                        │
│                                              │
│  Requests distributed via round-robin        │
└──────────────────────────────────────────────┘

Zero-Downtime Reload

┌──────────────────────────────────────────────┐
│  BEFORE: [W1] [W2] [W3] [W4]                 │
│         │                                    │
│         ▼                                    │
│  STEP 1: stop W1, start W1'                  │
│  [W1'] [W2] [W3] [W4]  ← 3 available        │
│         │                                    │
│         ▼                                    │
│  STEP 2: stop W2, start W2'                  │
│  [W1'] [W2'] [W3] [W4] ← 3 available        │
│         │                                    │
│         ▼                                    │
│  ... until all replaced                      │
│  [W1'] [W2'] [W3'] [W4']                     │
│                                              │
│  At least one instance always running        │
└──────────────────────────────────────────────┘

Graceful Shutdown Flow

┌──────────────────────────────────────────────┐
│  pm2 reload sends SIGINT                     │
│         │                                    │
│         ▼                                    │
│  Application catches SIGINT                  │
│         │                                    │
│         ▼                                    │
│  server.close()                              │
│  (stop accepting new connections)            │
│         │                                    │
│         ▼                                    │
│  Finish in-flight requests                   │
│         │                                    │
│         ▼                                    │
│  Close DB connections                        │
│         │                                    │
│         ▼                                    │
│  process.exit(0)                             │
│         │                                    │
│         ▼                                    │
│  PM2 starts replacement instance             │
└──────────────────────────────────────────────┘

Crash Restart Behavior

┌──────────────────────────────────────────────┐
│  Process crashes                             │
│         │                                    │
│         ▼                                    │
│  autorestart: true                           │
│         │                                    │
│         ▼                                    │
│  Wait restart_delay (e.g., 2000ms)           │
│         │                                    │
│         ▼                                    │
│  Restart instance                            │
│         │                                    │
│         ▼                                    │
│  If crashes within min_uptime repeatedly:    │
│  - default: stop after max_restarts          │
│  - unstable_restarts: 10000 → keep going     │
└──────────────────────────────────────────────┘

Summary

ItemValue
PM2 purposeProcess manager for Node.js
Cluster modeOne instance per CPU core, shared port
Enable cluster-i max or instances: 'max'
Zero-downtime reloadpm2 reload (one by one)
Config fileecosystem.config.js
Graceful shutdownHandle SIGINT, close server, exit
Memory limitmax_memory_restart
Crash loop survivalunstable_restarts
Monitoringpm2 status, pm2 monit, pm2 logs
Dockerpm2-runtime

Key takeaways:

  • Cluster mode multiplies throughput. One PM2 instance per CPU core, sharing the server port through Node.js cluster. The application must be stateless, or instances will behave inconsistently .
  • pm2 reload is the deployment command. It restarts instances one by one, keeping at least one running at all times. pm2 restart kills everything and causes downtime .
  • Graceful shutdown is required for reload to work. The application must handle SIGINT, stop accepting new connections, finish in-flight requests, and exit. If it hangs, PM2 falls back to a hard restart .
  • ecosystem.config.js centralizes production configuration. Instance count, environment variables, memory limits, log paths, and restart behavior all live in one version-controlled file .
  • max_memory_restart contains leaks. It does not fix the leak, but it prevents one instance from consuming the server’s memory and triggering the OOM killer .
  • unstable_restarts overrides crash loop detection. Without it, PM2 stops restarting a process that crashes too quickly, which can leave the application down during a transient failure .
  • cwd prevents relative path failures. PM2 does not run from the script’s directory by default. Setting cwd ensures config files and assets resolve correctly .

Remember: PM2 is the operational layer that turns a Node.js script into a production service. Cluster mode uses the hardware you paid for. Reload keeps the service available during deployments. The ecosystem file makes the configuration reproducible. Graceful shutdown ensures that reload does not drop requests. And the monitoring commands give you the visibility to know when something is wrong. None of these features work in isolation—cluster mode without statelessness causes session bugs, reload without graceful shutdown causes dropped requests, and a memory limit without a leak fix just delays the inevitable. PM2 provides the mechanisms; the application must provide the discipline.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!