
Your Discord bot will crash now and then. A database connection drops, an unhandled edge case in your code throws an exception, a request to Discord's API times out. What matters is how quickly the bot recovers. PM2 restarts it automatically, and a few settings decide how often and how fast.
PM2 Ecosystem Configuration
Instead of simple pm2 start commands, use an ecosystem file for production configuration:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'discord-bot',
script: 'src/index.js',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '256M', // Auto-restart if memory exceeds 256 MB
restart_delay: 5000, // 5s delay between restart attempts
max_restarts: 10, // Give up at the 10th unstable restart
min_uptime: '30s', // A run shorter than 30s counts as an unstable restart
env: {
NODE_ENV: 'production',
DISCORD_TOKEN: process.env.DISCORD_TOKEN
},
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: '/var/log/bot/error.log',
out_file: '/var/log/bot/out.log',
combine_logs: false
}]
};
Start with:
pm2 start ecosystem.config.js
pm2 save
PM2 creates a missing log folder by itself, but only where the user that runs PM2 may write. As a non-root user it cannot create /var/log/bot and stops with Could not create folder, so point error_file and out_file at a folder your user owns, or leave both out to log to ~/.pm2/logs. To bring the bot back after a reboot, also run pm2 startup once: as root it sets up the boot service itself, as any other user it prints a sudo command for you to run.
A Telegram bot built with grammY runs under PM2 the same way; our Telegram bot hosting guide covers that setup and the systemd alternative.
If the same VPS runs n8n, see our guide to running n8n and a Discord bot on one VPS for memory caps, ports and calling workflows from the bot.
The max_restarts Setting
This is critical for preventing crash loops. If your bot crashes repeatedly in a short window (Discord token revoked, database down), PM2 should stop trying to restart and alert you rather than spam restart attempts.
max_restarts: 10, // Errored at the 10th unstable restart (PM2's default is 16)
min_uptime: '30s', // A run shorter than 30s counts as unstable (PM2's default is 1 second)
Every run that ends within 30 seconds of its start counts as an unstable restart. At the 10th one, PM2 marks the bot as errored and stops restarting it. The check in PM2's source is unstable_restarts >= max_restarts, so the limit is reached at the 10th crash, not after it. With the alert module from the monitoring section you get a message at that moment, instead of a bot that spins in a crash loop.
This file also sets restart_delay, and in PM2 7.1.1 that has a side effect: right after PM2 sets the status to errored, its restart delay code changes it to waiting restart, but no restart follows. So if pm2 list still shows waiting restart long after the 5-second delay, PM2 has given up, and ~/.pm2/pm2.log has a line with had too many unstable restarts (10). Stopped.
PM2 only counts these unstable restarts in the first 300 seconds (min_uptime times max_restarts) after you start or restart the bot, so a bot that ran for hours before its crash loop never reaches errored; our guide to a PM2 app that keeps restarting explains the counter.
Deploying New Code
During a deployment you pull the new code and restart the bot:
git pull && pm2 reload discord-bot
A single Discord bot runs in PM2's fork mode, and there pm2 reload works like pm2 restart: PM2 sends SIGINT, gives the bot 1.6 seconds by default (kill_timeout) to exit, then sends SIGKILL and starts the new code. The 0-second-downtime reload in PM2's docs is a cluster mode feature, and cluster mode would start several copies of your bot. So expect a short gap, and handle SIGINT in your bot: close the gateway connection and your database cleanly, then exit.
Monitoring PM2 Status
pm2 status # Current state of all processes
pm2 logs discord-bot # Live log tail
pm2 monit # Interactive dashboard in terminal
For alerts in a Discord channel, the pm2-discord module posts PM2 events to a webhook:
pm2 install pm2-discord
pm2 set pm2-discord:discord_url https://discord.com/api/webhooks/YOUR_WEBHOOK_URL
pm2 set pm2-discord:log false
pm2 set pm2-discord:exit true
Check its defaults before you rely on it. Out of the box, pm2-discord forwards every line your processes write to stdout (log) plus the exception, stop, kill and restart overlimit events, but not exit or restart. A crash reaches PM2 as an exit, and PM2 only sends restart when you run pm2 restart yourself. The last two lines above stop the log stream and turn on a message for every exit; restart overlimit is the message that PM2 gave up. The pm2-discord README asks for PM2 6, while npm install -g pm2 gives you 7.1.1 today, so check once that pm2 restart discord-bot posts an exit message.
If you would rather not run PM2 yourself, the paid plans of Space-Node's Discord bot hosting restart a crashed bot for you. PM2 is the tool for a bot on your own VPS.
Sources
All opened on 31 October 2026:
- PM2 docs: Configuration File: max_restarts (default 16), min_uptime, restart_delay, kill_timeout (1600 ms), combine_logs
- PM2 docs: Graceful Start/Shutdown: SIGINT first, SIGKILL after 1.6 seconds
- PM2 docs: Cluster Mode: the 0-second-downtime reload
- PM2 docs: Startup Script: pm2 startup prints the sudo command to run, then pm2 save
- PM2 7.1.1 source, lib/API/Startup.js: as a non-root user, pm2 startup only prints the sudo command
- PM2 7.1.1 source, lib/God.js: unstable restart window, errored at
unstable_restarts >= max_restarts, restart_delay setswaiting restartafterwards - PM2 7.1.1 source, lib/God/Methods.js: a manual restart resets the counter
- PM2 7.1.1 source, lib/God/Reload.js: reload in fork mode falls back to restart
- PM2 7.1.1 source, lib/Common.js: PM2 creates missing log folders or stops with "Could not create folder"
- PM2 7.1.1 source, lib/API.js: restart, stop and delete events come from commands
- pm2 on npm: 7.1.1 is the latest release
- pm2-discord README: install commands, event defaults, PM2 6 requirement
- pm2-discord 1.0.0 source, src/config.mts: default event settings