PM2 Complete Guide 2026 — Node.js Process Manager for Production Servers

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

When a Node.js process crashes, your app goes down until someone manually restarts it. PM2 fixes this: it keeps your app running, restarts on crash, uses all CPU cores through clustering, and provides log rotation and monitoring out of the box. For teams running Node.js on VPS or EC2 without Kubernetes, PM2 is the simplest production-grade process manager available in 2026.

Installation and Basic Commands

npm install -g pm2
 
# Start an application
pm2 start app.js
pm2 start dist/server.js --name "api"
pm2 start "npm run start" --name "nextjs"
 
# Cluster mode — utilize all CPU cores
pm2 start app.js -i max     # instances = CPU count
pm2 start app.js -i 4       # exactly 4 instances
 
# Status and monitoring
pm2 status
pm2 list
pm2 monit                   # Real-time CPU and memory dashboard
 
# Logs
pm2 logs              # Stream all app logs
pm2 logs api          # Stream specific app logs
pm2 logs --lines 200  # Last 200 lines
 
# Lifecycle
pm2 reload api        # Zero-downtime reload (cluster mode)
pm2 restart api       # Hard restart (brief downtime)
pm2 stop api
pm2 delete api

Ecosystem Configuration

The ecosystem file is the right way to configure PM2 for production:

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'api-server',
      script: 'dist/server.js',
 
      // Cluster mode — use all CPU cores
      instances: 'max',
      exec_mode: 'cluster',
 
      // Environment variables per mode
      env: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
 
      // Restart behavior
      max_restarts: 10,
      restart_delay: 4000,
      exp_backoff_restart_delay: 100,  // Exponential backoff between restarts
 
      // Auto-restart if memory exceeds 1GB
      max_memory_restart: '1G',
 
      // Logging
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      out_file: './logs/out.log',
      error_file: './logs/error.log',
      merge_logs: true,
 
      // Graceful shutdown
      wait_ready: true,          // Wait for app to send 'ready'
      listen_timeout: 10000,     // Wait 10s for app to be ready
      kill_timeout: 5000,        // Allow 5s for graceful shutdown
 
      // Source maps for better error stack traces
      source_map_support: true,
    },
 
    // Background worker
    {
      name: 'email-worker',
      script: 'dist/workers/email.js',
      instances: 2,
      exec_mode: 'cluster',
      cron_restart: '0 4 * * *',  // Daily restart at 4 AM
      max_memory_restart: '512M',
    },
  ],
}
# Start with production environment
pm2 start ecosystem.config.js --env production
 
# Reload all apps in ecosystem
pm2 reload ecosystem.config.js --env production
 
# Save and restore process list across reboots
pm2 save

Zero-Downtime Reloads

PM2 cluster mode enables zero-downtime deploys — it restarts one worker at a time while others keep serving traffic:

# Zero-downtime reload (maintains availability in cluster mode)
pm2 reload api
 
# Deploy script with graceful reload
#!/bin/bash
set -euo pipefail
 
cd /app
git pull origin main
npm ci --production
npm run build
pm2 reload ecosystem.config.js --env production --update-env
 
echo "Deploy complete"

Your Node.js app should signal readiness to PM2 and handle SIGINT gracefully:

// server.ts
import express from 'express'
import { db } from './db'
import { redis } from './redis'
 
const app = express()
 
const server = app.listen(3000, () => {
  console.log('Server listening on port 3000')
 
  // Signal PM2 that the app is ready to receive traffic
  if (process.send) {
    process.send('ready')
  }
})
 
// Graceful shutdown on SIGINT (sent by PM2 during reload)
process.on('SIGINT', async () => {
  console.log('SIGINT received — graceful shutdown starting')
 
  server.close(async () => {
    await db.end()
    await redis.quit()
    console.log('Graceful shutdown complete')
    process.exit(0)
  })
 
  // Force exit after timeout if shutdown hangs
  setTimeout(() => {
    console.error('Graceful shutdown timed out — forcing exit')
    process.exit(1)
  }, 10000)
})

Startup Scripts (Survive Server Reboots)

# Generate systemd startup script (run as your app user)
pm2 startup
 
# PM2 outputs a command like:
# sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu
# Run that command, then:
 
pm2 save   # Save current process list to ~/.pm2/dump.pm2
 
# Verify it starts on boot
sudo systemctl status pm2-ubuntu
sudo systemctl enable pm2-ubuntu

Log Management

# Install log rotation module
pm2 install pm2-logrotate
 
# Configure rotation
pm2 set pm2-logrotate:max_size 50M     # Rotate when file exceeds 50MB
pm2 set pm2-logrotate:retain 7         # Keep 7 rotated files
pm2 set pm2-logrotate:compress true    # Gzip rotated files
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'  # Daily at midnight
 
# Flush current logs
pm2 flush
 
# View logs with timestamp
pm2 logs api --timestamp

Remote Deployment

// ecosystem.config.js — deploy section
module.exports = {
  apps: [...],
  deploy: {
    production: {
      user: 'ubuntu',
      host: ['my-server.com'],
      ref: 'origin/main',
      repo: 'git@github.com:myorg/app.git',
      path: '/app',
      'post-deploy': [
        'npm ci --production',
        'npm run build',
        'pm2 reload ecosystem.config.js --env production',
      ].join(' && '),
    },
  },
}
# Initial server setup
pm2 deploy production setup
 
# Deploy on every release
pm2 deploy production

Cluster Mode and Shared State

In cluster mode, multiple Node.js processes share one port but each has its own memory:

// Do NOT store state in-process with cluster mode
// These break with multiple workers:
const sessions = new Map()       // Only exists in one worker
let cachedConfig = null          // Each worker caches separately
const jobQueue: Job[] = []       // Jobs only visible to one worker
 
// DO use external shared state:
// Sessions:    Redis (connect-redis)
// Cache:       Redis (ioredis)
// Locks:       Redlock
// Job queues:  BullMQ (backed by Redis)
// Pub/sub:     Redis pub/sub or socket.io-redis

Common Mistakes

  • Using pm2 restart instead of pm2 reload — restart causes a brief downtime; reload is zero-downtime for cluster mode
  • Not saving process list with pm2 save — processes do not survive reboots unless saved to the PM2 dump file
  • Storing session data in memory with cluster mode — each worker has isolated memory; sessions disappear on the next request
  • No memory limit set — without max_memory_restart, a memory leak will eventually crash the entire server
  • Not testing graceful shutdown — send SIGINT manually and verify in-flight requests complete before the process exits

Best Practices

  • Always use exec_mode: 'cluster' with instances: 'max' on multi-core servers to utilize all available CPU
  • Set wait_ready: true and call process.send('ready') so PM2 only sends traffic to fully initialized workers
  • Use pm2-logrotate to prevent log files from filling up disks — rotate at 50MB and keep 7 days
  • Monitor with pm2 monit during load tests to find memory leaks and CPU bottlenecks before they affect production
  • Run pm2 logs --err to stream only error logs during incident response — faster than grepping combined logs

Key Takeaways

  • PM2 cluster mode starts one Node.js process per CPU core and load-balances incoming connections automatically
  • pm2 reload sends SIGINT to one worker at a time and waits for it to finish serving requests before replacing it — zero downtime
  • The ecosystem config file defines all apps, their environments, restart policies, and log paths in version-controlled YAML
  • pm2 startup + pm2 save makes your applications survive server reboots via systemd on Ubuntu and Debian
  • max_memory_restart protects against memory leaks — PM2 automatically restarts the worker before it crashes the server
  • Cluster mode requires external shared state (Redis) for sessions, caches, and queues — in-process storage does not work
  • PM2 log rotation prevents disks from filling up; rotate at 50MB and retain 7 files for most production use cases
  • wait_ready: true + process.send('ready') ensures PM2 only routes traffic to workers after they complete initialization

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading