Node.js 66 🟢 Production Process Management and Monitoring with PM2
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
| Command | Purpose |
|---|---|
pm2 start ecosystem.config.js | Start all apps in config |
pm2 start app.js -i max | Start in cluster mode on all cores |
pm2 status | Show process table |
pm2 monit | Real-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 startup | Configure boot startup |
pm2 save | Persist current process list |
Ecosystem File Options
| Option | Type | Purpose |
|---|---|---|
name | string | Application name |
script | string | Path to entry file |
cwd | string | Working directory |
instances | number or 'max' | Cluster instance count |
exec_mode | 'cluster' or 'fork' | Execution mode |
autorestart | boolean | Restart on crash (default true) |
max_memory_restart | string | Memory threshold for restart |
kill_timeout | number | ms before SIGKILL after SIGINT |
unstable_restarts | number | Override crash loop detection |
restart_delay | number | ms between restarts |
merge_logs | boolean | Combine logs from all instances |
Cluster vs Fork Mode
| Mode | Behavior | Use Case |
|---|---|---|
fork | Single process, no port sharing | Development, non-network apps |
cluster | Multiple processes, shared port | Production HTTP/TCP servers |
Reload vs Restart
| Command | Behavior | Downtime |
|---|---|---|
pm2 reload | Restarts instances one by one | None |
pm2 restart | Kills and restarts all | Brief |
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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Sessions lost after reload | In-memory session store | Use Redis or database |
| Process stops restarting | unstable_restarts default too low | Set to 10000 |
| App works manually but not in PM2 | Wrong cwd or env vars | Set cwd and use env_production |
| Reload hangs then restarts | Graceful shutdown too slow | Increase kill_timeout |
| Memory grows until OOM | No max_memory_restart | Set a threshold |
| PM2 exits immediately in Docker | Used pm2 start | Use 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
| Item | Value |
|---|---|
| PM2 purpose | Process manager for Node.js |
| Cluster mode | One instance per CPU core, shared port |
| Enable cluster | -i max or instances: 'max' |
| Zero-downtime reload | pm2 reload (one by one) |
| Config file | ecosystem.config.js |
| Graceful shutdown | Handle SIGINT, close server, exit |
| Memory limit | max_memory_restart |
| Crash loop survival | unstable_restarts |
| Monitoring | pm2 status, pm2 monit, pm2 logs |
| Docker | pm2-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 reloadis the deployment command. It restarts instances one by one, keeping at least one running at all times.pm2 restartkills 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.jscentralizes production configuration. Instance count, environment variables, memory limits, log paths, and restart behavior all live in one version-controlled file .max_memory_restartcontains leaks. It does not fix the leak, but it prevents one instance from consuming the server’s memory and triggering the OOM killer .unstable_restartsoverrides crash loop detection. Without it, PM2 stops restarting a process that crashes too quickly, which can leave the application down during a transient failure .cwdprevents relative path failures. PM2 does not run from the script’s directory by default. Settingcwdensures 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!