Optimize Node.js application performance with PM2 clustering and memory management

Intermediate 25 min Apr 03, 2026 15 views
Ubuntu 24.04 Ubuntu 22.04 Debian 12 AlmaLinux 9 Rocky Linux 9 Fedora 41

Learn how to optimize Node.js application performance using PM2 process manager with clustering, memory limits, and monitoring. This tutorial covers production deployment with systemd integration and advanced performance tuning.

Prerequisites

  • Root or sudo access
  • Node.js and npm installed
  • Basic understanding of Node.js applications
  • Familiarity with Linux process management

What this solves

PM2 is a production-grade process manager for Node.js applications that enables clustering, automatic restarts, and memory management. This tutorial helps you optimize Node.js performance by leveraging multiple CPU cores, setting memory limits, and implementing proper monitoring for high-traffic production environments.

Step-by-step installation

Update system packages and install Node.js

Start by updating your package manager and installing Node.js if not already present.

sudo apt update && sudo apt upgrade -y
sudo apt install -y nodejs npm curl build-essential
sudo dnf update -y
sudo dnf install -y nodejs npm curl gcc-c++ make

Install PM2 globally

Install PM2 process manager globally to manage Node.js applications across the system.

sudo npm install -g pm2
pm2 --version

Create a sample Node.js application

Create a test application to demonstrate PM2 clustering and optimization features.

mkdir -p /opt/myapp
cd /opt/myapp
const express = require('express');
const cluster = require('cluster');
const app = express();
const port = process.env.PORT || 3000;

// Memory-intensive route for testing
app.get('/memory-test', (req, res) => {
    const data = new Array(1000000).fill('test data');
    res.json({ 
        pid: process.pid,
        memory: process.memoryUsage(),
        dataLength: data.length
    });
});

// CPU-intensive route for testing
app.get('/cpu-test', (req, res) => {
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
        result += Math.sqrt(i);
    }
    res.json({ 
        pid: process.pid,
        result: result,
        cpus: require('os').cpus().length
    });
});

app.get('/', (req, res) => {
    res.json({ 
        message: 'Hello from PM2!',
        pid: process.pid,
        uptime: process.uptime()
    });
});

app.listen(port, () => {
    console.log(Server running on port ${port}, PID: ${process.pid});
});

module.exports = app;
{
  "name": "myapp",
  "version": "1.0.0",
  "description": "PM2 optimized Node.js application",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}

Install application dependencies

Install the required Node.js packages for the application.

cd /opt/myapp
npm install

Configure PM2 with clustering

Create a PM2 ecosystem configuration file that defines clustering, memory limits, and performance settings.

module.exports = {
  apps: [{
    name: 'myapp',
    script: './app.js',
    instances: 'max', // Use all available CPU cores
    exec_mode: 'cluster',
    
    // Memory management
    max_memory_restart: '500M',
    
    // Performance settings
    node_args: '--max-old-space-size=512 --optimize-for-size',
    
    // Environment variables
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    
    // Logging
    log_file: '/var/log/myapp/combined.log',
    out_file: '/var/log/myapp/out.log',
    error_file: '/var/log/myapp/error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    
    // Restart settings
    restart_delay: 1000,
    max_restarts: 10,
    min_uptime: '10s',
    
    // Monitoring
    pmx: true,
    
    // Advanced settings
    kill_timeout: 5000,
    wait_ready: true,
    listen_timeout: 8000,
    
    // Cron restart for memory cleanup
    cron_restart: '0 2   *' // Restart daily at 2 AM
  }]
};

Create log directories with proper permissions

Set up log directories with correct ownership and permissions for PM2 logging.

sudo mkdir -p /var/log/myapp
sudo chown -R $USER:$USER /var/log/myapp
chmod 755 /var/log/myapp
Never use chmod 777. It gives every user on the system full access to your files. Instead, use specific ownership with chown and minimal permissions like 755 for directories.

Start application with PM2 clustering

Launch the application using the ecosystem configuration with clustering enabled.

cd /opt/myapp
pm2 start ecosystem.config.js
pm2 status
pm2 info myapp

Configure Node.js garbage collection optimization

Create a separate configuration for advanced memory management and garbage collection tuning.

module.exports = {
  apps: [{
    name: 'myapp-optimized',
    script: './app.js',
    instances: 4, // Fixed number for production
    exec_mode: 'cluster',
    
    // Advanced memory settings
    max_memory_restart: '400M',
    
    // Garbage collection optimization
    node_args: [
      '--max-old-space-size=384',
      '--gc-interval=100',
      '--optimize-for-size',
      '--use-idle-notification',
      '--expose-gc'
    ].join(' '),
    
    // Environment
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      UV_THREADPOOL_SIZE: 8
    },
    
    // Advanced monitoring
    monitoring: true,
    pmx: true,
    
    // Instance distribution
    increment_var: 'PORT',
    
    // Health checks
    health_check_grace_period: 3000,
    
    // Logging with rotation
    log_file: '/var/log/myapp/combined.log',
    out_file: '/var/log/myapp/out.log',
    error_file: '/var/log/myapp/error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    merge_logs: true,
    
    // Restart policies
    restart_delay: 2000,
    max_restarts: 5,
    min_uptime: '30s',
    
    // Performance monitoring
    instance_var: 'INSTANCE_ID',
    
    // Auto restart on file changes (disable in production)
    watch: false,
    ignore_watch: ['node_modules', 'logs']
  }]
};

Configure PM2 monitoring and metrics

Enable PM2 monitoring features and set up log rotation for long-running applications.

# Install PM2 log rotate module
pm2 install pm2-logrotate

Configure log rotation

pm2 set pm2-logrotate:retain 7 pm2 set pm2-logrotate:max_size 100M pm2 set pm2-logrotate:compress true pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss pm2 set pm2-logrotate:rotateInterval '0 0 *' pm2 set pm2-logrotate:rotateModule true

Set up PM2 with systemd for production

Configure PM2 to start automatically with systemd and survive system reboots.

# Generate systemd startup script
sudo pm2 startup systemd -u $USER --hp $HOME

Save current PM2 process list

pm2 save

Verify systemd integration

sudo systemctl status pm2-$USER sudo systemctl enable pm2-$USER

Configure system-level optimizations

Optimize kernel parameters for high-performance Node.js applications running under PM2.

# Network optimizations for Node.js
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 5000
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 9

File descriptor limits

fs.file-max = 1048576 fs.nr_open = 1048576

Memory management

vm.swappiness = 10 vm.dirty_ratio = 15 vm.dirty_background_ratio = 5
sudo sysctl -p /etc/sysctl.d/99-nodejs-optimization.conf

Configure system resource limits

Set appropriate resource limits for Node.js processes to prevent resource exhaustion.

# Node.js resource limits
* soft nofile 65535
* hard nofile 65535
* soft nproc 32768
* hard nproc 32768
root soft nofile 65535
root hard nofile 65535

Create PM2 monitoring dashboard

Set up PM2 monitoring commands and create a simple monitoring script.

#!/bin/bash

PM2 Application Monitoring Script

echo "=== PM2 Status ===" pm2 status echo -e "\n=== Memory Usage ===" pm2 monit --no-interaction | head -20 echo -e "\n=== Application Logs (last 10 lines) ===" pm2 logs --lines 10 --nostream echo -e "\n=== System Resources ===" echo "CPU Usage: $(top -bn1 | grep load | awk '{printf "%.2f%%\n", $(NF-2)}')" echo "Memory Usage: $(free | grep Mem | awk '{printf "%.2f%%\n", ($3/$2) * 100.0}')" echo "Disk Usage: $(df -h | grep -E '^/dev/' | awk '{print $5}' | head -1)" echo -e "\n=== PM2 Process Details ===" pm2 describe myapp-optimized | grep -E '(status|uptime|restarts|memory|cpu)' echo -e "\n=== Recent Errors ===" pm2 logs --err --lines 5 --nostream
chmod +x /opt/myapp/monitor.sh

Advanced PM2 optimization configuration

Configure load balancing and scaling

Set up dynamic scaling and load balancing policies for PM2 cluster mode.

module.exports = {
  apps: [{
    name: 'myapp-scaled',
    script: './app.js',
    instances: 0, // Will be set dynamically
    exec_mode: 'cluster',
    
    // Auto-scaling configuration
    max_memory_restart: '300M',
    
    // Performance tuning
    node_args: '--max-old-space-size=256 --gc-interval=100',
    
    // Environment optimizations
    env: {
      NODE_ENV: 'production',
      UV_THREADPOOL_SIZE: 16,
      NODE_OPTIONS: '--enable-source-maps'
    },
    
    // Cluster settings
    kill_timeout: 3000,
    wait_ready: true,
    listen_timeout: 5000,
    
    // Health monitoring
    health_check_grace_period: 2000,
    
    // Auto restart conditions
    restart_delay: 1500,
    max_restarts: 3,
    min_uptime: '20s',
    
    // Custom metrics
    pmx: {
      http: true,
      errors: true,
      custom_probes: true,
      network: true,
      ports: true
    }
  }]
};

Implement PM2 event loop monitoring

Add event loop monitoring to detect and prevent blocking operations.

const pmx = require('pmx');

// Custom metrics
const probe = pmx.probe();

// Event loop lag monitoring
const eventLoopLag = probe.metric({
  name: 'Event Loop Lag',
  unit: 'ms'
});

// Memory usage tracking
const memoryUsage = probe.metric({
  name: 'Memory Usage',
  unit: 'MB'
});

// Request rate tracking
let requestCount = 0;
const requestRate = probe.counter({
  name: 'Request Rate'
});

// Monitor event loop lag
setInterval(() => {
  const start = process.hrtime();
  setImmediate(() => {
    const delta = process.hrtime(start);
    const lag = (delta[0] * 1000) + (delta[1] / 1e6);
    eventLoopLag.set(lag);
    
    // Alert if lag is too high
    if (lag > 100) {
      console.warn(High event loop lag detected: ${lag}ms);
    }
  });
}, 5000);

// Monitor memory usage
setInterval(() => {
  const usage = process.memoryUsage();
  memoryUsage.set(Math.round(usage.rss / 1024 / 1024));
  
  // Trigger GC if available and memory is high
  if (global.gc && usage.heapUsed > 200  1024  1024) {
    global.gc();
  }
}, 10000);

// Export for use in main app
module.exports = {
  requestRate: requestRate
};

Verify your setup

# Check PM2 status
pm2 status
pm2 info myapp-optimized

Test application endpoints

curl http://localhost:3000/ curl http://localhost:3000/cpu-test curl http://localhost:3000/memory-test

Monitor real-time metrics

pm2 monit

Check logs

pm2 logs --lines 20

Verify systemd integration

sudo systemctl status pm2-$USER

Test clustering is working (should show different PIDs)

for i in {1..5}; do curl -s http://localhost:3000/ | grep pid; done

Check system optimizations

sysctl net.core.somaxconn ulimit -n

Run monitoring script

/opt/myapp/monitor.sh

Performance tuning and scaling commands

# Scale application dynamically
pm2 scale myapp-optimized 8

Reload without downtime

pm2 reload myapp-optimized

Reset restart count

pm2 reset myapp-optimized

Show detailed memory usage

pm2 show myapp-optimized

Monitor specific metrics

pm2 monit --no-interaction

Flush all logs

pm2 flush

Kill and restart specific instance

pm2 restart myapp-optimized --update-env

Common issues

SymptomCauseFix
Application won't startPort already in useCheck with sudo lsof -i :3000 and kill conflicting process
High memory usageMemory leaks or large objectsLower max_memory_restart, add garbage collection tuning
Frequent restartsApplication crashes or memory limitCheck logs with pm2 logs, increase memory limit gradually
Clustering not workingApp not cluster-compatibleEnsure app doesn't store state in memory, use exec_mode: 'fork' for single instance
PM2 doesn't start on bootSystemd not configuredRun pm2 startup and pm2 save again
Log files permission deniedWrong ownershipFix with sudo chown -R $USER:$USER /var/log/myapp
High CPU usageEvent loop blockingMonitor with custom probes, optimize async operations
Can't connect to applicationFirewall blocking portOpen port with sudo ufw allow 3000

Next steps

Automated install script

Run this to automate the entire setup

#pm2 #nodejs #clustering #performance #memory-management

Need help?

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.

Talk to an engineer