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
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
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
| Symptom | Cause | Fix |
|---|---|---|
| Application won't start | Port already in use | Check with sudo lsof -i :3000 and kill conflicting process |
| High memory usage | Memory leaks or large objects | Lower max_memory_restart, add garbage collection tuning |
| Frequent restarts | Application crashes or memory limit | Check logs with pm2 logs, increase memory limit gradually |
| Clustering not working | App not cluster-compatible | Ensure app doesn't store state in memory, use exec_mode: 'fork' for single instance |
| PM2 doesn't start on boot | Systemd not configured | Run pm2 startup and pm2 save again |
| Log files permission denied | Wrong ownership | Fix with sudo chown -R $USER:$USER /var/log/myapp |
| High CPU usage | Event loop blocking | Monitor with custom probes, optimize async operations |
| Can't connect to application | Firewall blocking port | Open port with sudo ufw allow 3000 |
Next steps
- Set up NGINX reverse proxy with SSL certificates to secure your PM2 applications
- Configure Linux system resource limits with systemd for better resource management
- Monitor Node.js applications with Prometheus and Grafana for advanced observability
- Configure Node.js application logging with Winston for better log management
- Set up Node.js application security with Helmet and rate limiting for production hardening
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# Configuration
readonly APP_NAME="myapp"
readonly APP_DIR="/opt/myapp"
readonly LOG_DIR="/var/log/myapp"
readonly USER="${SUDO_USER:-$(whoami)}"
readonly NODE_VERSION="18"
print_step() { echo -e "${GREEN}[$1/9] $2${NC}"; }
print_warning() { echo -e "${YELLOW}WARNING: $1${NC}"; }
print_error() { echo -e "${RED}ERROR: $1${NC}"; }
usage() {
echo "Usage: $0 [PORT]"
echo " PORT: Application port (default: 3000)"
exit 1
}
cleanup_on_error() {
print_error "Installation failed. Cleaning up..."
pm2 delete myapp 2>/dev/null || true
rm -rf "$APP_DIR" 2>/dev/null || true
exit 1
}
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root or with sudo"
exit 1
fi
if ! command -v curl &> /dev/null; then
print_error "curl is required but not installed"
exit 1
fi
}
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
print_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
if ! command -v dnf &> /dev/null; then
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
fi
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
}
install_nodejs() {
print_step 1 "Installing Node.js and dependencies"
$PKG_UPDATE
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL curl gnupg2 software-properties-common build-essential
curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
$PKG_INSTALL nodejs
else
$PKG_INSTALL curl gcc-c++ make
curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash -
$PKG_INSTALL nodejs
fi
node --version
npm --version
}
install_pm2() {
print_step 2 "Installing PM2 globally"
npm install -g pm2
pm2 --version
}
create_application() {
print_step 3 "Creating Node.js application"
mkdir -p "$APP_DIR"
cd "$APP_DIR"
cat > app.js << 'EOF'
const express = require('express');
const cluster = require('cluster');
const app = express();
const port = process.env.PORT || 3000;
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
});
});
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;
EOF
cat > package.json << 'EOF'
{
"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"
}
}
EOF
chown -R "$USER:$USER" "$APP_DIR"
chmod 755 "$APP_DIR"
chmod 644 "$APP_DIR"/*.js "$APP_DIR"/*.json
}
install_dependencies() {
print_step 4 "Installing application dependencies"
cd "$APP_DIR"
sudo -u "$USER" npm install
}
create_pm2_config() {
print_step 5 "Creating PM2 ecosystem configuration"
cat > "$APP_DIR/ecosystem.config.js" << EOF
module.exports = {
apps: [{
name: 'myapp',
script: './app.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '500M',
node_args: '--max-old-space-size=512 --optimize-for-size',
env: {
NODE_ENV: 'production',
PORT: ${PORT}
},
log_file: '${LOG_DIR}/combined.log',
out_file: '${LOG_DIR}/out.log',
error_file: '${LOG_DIR}/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
restart_delay: 1000,
max_restarts: 10,
min_uptime: '10s',
pmx: true,
kill_timeout: 5000,
wait_ready: true,
listen_timeout: 8000,
cron_restart: '0 2 * * *'
}]
};
EOF
chown "$USER:$USER" "$APP_DIR/ecosystem.config.js"
chmod 644 "$APP_DIR/ecosystem.config.js"
}
setup_logging() {
print_step 6 "Setting up log directories"
mkdir -p "$LOG_DIR"
chown -R "$USER:$USER" "$LOG_DIR"
chmod 755 "$LOG_DIR"
}
start_application() {
print_step 7 "Starting application with PM2"
cd "$APP_DIR"
sudo -u "$USER" pm2 start ecosystem.config.js
sudo -u "$USER" pm2 save
}
setup_systemd() {
print_step 8 "Setting up PM2 systemd service"
sudo -u "$USER" pm2 startup systemd -u "$USER" --hp "/home/$USER" | grep -E '^sudo' | sh
}
configure_firewall() {
print_step 9 "Configuring firewall"
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
if command -v ufw &> /dev/null; then
ufw allow "$PORT"/tcp
fi
else
if command -v firewall-cmd &> /dev/null && systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-port="$PORT"/tcp
firewall-cmd --reload
fi
fi
}
verify_installation() {
echo -e "\n${GREEN}=== Installation Complete ===${NC}"
echo -e "Application: http://localhost:$PORT"
echo -e "Test endpoints:"
echo -e " - http://localhost:$PORT/"
echo -e " - http://localhost:$PORT/cpu-test"
echo -e " - http://localhost:$PORT/memory-test"
echo -e "\nPM2 Status:"
sudo -u "$USER" pm2 status
echo -e "\nUseful PM2 commands:"
echo -e " - pm2 status # View app status"
echo -e " - pm2 logs # View logs"
echo -e " - pm2 reload myapp # Reload app"
echo -e " - pm2 stop myapp # Stop app"
}
main() {
readonly PORT="${1:-3000}"
if [[ ! "$PORT" =~ ^[0-9]+$ ]] || [[ "$PORT" -lt 1 ]] || [[ "$PORT" -gt 65535 ]]; then
print_error "Invalid port number: $PORT"
usage
fi
trap cleanup_on_error ERR
check_prerequisites
detect_distro
install_nodejs
install_pm2
create_application
install_dependencies
create_pm2_config
setup_logging
start_application
setup_systemd
configure_firewall
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh