PM2 App Keeps Restarting: Find the Cause and Fix It

Published on

PM2 restart count climbing or app errored? Read pm2 show and pm2.log to see why: crashes, memory limits, watch loops, busy ports, missing env vars or OOM.

Illustration of a man kneeling with his hands on his head beside server racks and a monitor that shows a 500 Internal Server Error

pm2 list says your app is online, but the ↺ column keeps climbing, the uptime never gets past a few seconds, or the status has switched to errored. Something ends the process and PM2 starts it again, and PM2 writes down why in a log most people never open. This guide reads that log first, then fixes each cause. The commands are for Ubuntu 24.04 and Debian 12 with PM2 7.1.1, the current release. On a Space-Node VPS you get full root access, so they work as written.

Quick answer: Run pm2 show my-app for the restart counters, then read ~/.pm2/pm2.log, where PM2 logs every exit. exited with code [1] is a crash: the stack trace in pm2 logs my-app --err --lines 100 --nostream often shows an unhandled promise rejection, EADDRINUSE or a missing environment variable. exceeds --max-memory-restart value is PM2's memory limit, Change detected on path is watch mode, and via signal [SIGKILL] is usually the kernel's OOM killer (sudo journalctl -k | grep -i 'killed process'). errored means PM2 gave up after 16 runs shorter than min_uptime, 1 second by default.

Step 1: Read the Restart Counters

Run PM2 as the user that started the app: every user has their own PM2 daemon and process list in ~/.pm2.

pm2 list
pm2 show my-app

↺ in pm2 list is the restart count. In pm2 show, look at:

  • restarts: every restart, including your own pm2 restart.
  • unstable restarts: exits within min_uptime of a start. If this climbs, the app dies during startup.
  • error log path: the file behind pm2 logs --err.
  • exec cwd: the folder the app runs in.
  • watch & reload, max memory restart and cron restart: settings that restart the app on purpose. The last two only show up when set.

After a fix, set the counters to zero with pm2 reset my-app and see whether the restarts come back.

Step 2: Find the Reason in pm2.log

The PM2 daemon logs every exit with its exit code and signal in ~/.pm2/pm2.log, plus the reason when its memory limit or watch mode restarted the app. pm2 logs my-app leaves this file out, so read it directly, next to the app's error log:

grep -E 'my-app|max-memory|unstable|still alive' ~/.pm2/pm2.log | tail -n 40
pm2 logs my-app --err --lines 100 --nostream

The extra grep words catch PM2 lines that name the process by its id or script path. --nostream prints the last lines (15 without --lines) and exits. A crash loop looks like this:

2026-10-30T09:41:07: PM2 log: App [my-app:0] exited with code [1] via signal [SIGINT]
2026-10-30T09:41:07: PM2 log: App [my-app:0] starting in -fork mode-
2026-10-30T09:41:07: PM2 log: App [my-app:0] online

PM2 prints via signal [SIGINT] whenever the process exited without a signal, so ignore it here. When a signal did end the process, Node reports no exit code and PM2 prints [0]. A restart that PM2 started itself has a Stopping app:my-app id:0 line before the exit.

In pm2.logWhat happenedSection
exited with code [1] or another non-zero codeThe app crashedCrashes
exited with code [0] via signal [SIGINT], no Stopping app: before itThe app ended by itselfFAQ
had too many unstable restarts (16). Stopped.PM2 gave up: erroredErrored
restarted because it exceeds --max-memory-restart valuePM2's memory limitMemory
via signal [SIGABRT]Node's heap limitMemory
Change detected on pathWatch modeWatch mode
via signal [SIGKILL], no still alive after before itKilled from outsideOOM killer

A SIGKILL right after Process with pid ... still alive after 1600ms is PM2's own: it stops an app with SIGINT and sends SIGKILL when the app outlives kill_timeout, 1,600 ms by default. That slows restarts down, but it doesn't cause them.

Errored: How max_restarts and min_uptime Work

PM2 counts an exit as an unstable restart when the run lasted less than min_uptime, 1,000 ms by default. After max_restarts of them, 16 by default, it logs had too many unstable restarts, sets the status to errored and stops. PM2's source adds a detail its docs leave out: unstable restarts only count in the first min_uptime times max_restarts milliseconds after a start or a manual restart, 16 seconds with the defaults. An app that crashes after a few seconds, or one that ran for days before the crashes began, never reaches errored. PM2 restarts it forever.

Fix the cause, then run pm2 restart my-app. To keep a crash loop from hammering your database in the meantime, make PM2 wait between attempts. An ecosystem file with the options from this guide:

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-app',
    script: './server.js',
    cwd: '/home/app/my-app',
    exp_backoff_restart_delay: 100,
    max_memory_restart: '500M',
    time: true,
    env: { NODE_ENV: 'production', PORT: 3000 }
  }]
};
pm2 delete my-app
pm2 start ecosystem.config.js
pm2 save

With exp_backoff_restart_delay, the status reads waiting restart between attempts, and each wait is half as long again as the last (100, 150, 225 ms and so on) up to 15 seconds. After 30 seconds of uptime it resets. time: true timestamps your app's log lines so they match pm2.log. For a bot, our PM2 guide for Discord bots uses a longer min_uptime.

PM2 also restarts on purpose: after any exit, a clean process.exit(0) included (see the FAQ), and on a cron_restart schedule, which pm2 restart my-app --cron-restart 0 switches off.

Crashes: Exceptions and Unhandled Rejections

Exit code 1 is an uncaught exception: Node prints the stack trace to stderr, which ends up in the error log. Read the last trace from the top: the message, then the first line in your own code.

Since Node.js 15, an unhandled promise rejection ends the process too. The default --unhandled-rejections mode, throw, raises it as an uncaught exception, so a rejected Error looks like a thrown one. A rejection with a plain value prints this:

UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "timeout".

Look for an await without try/catch in a route handler, timer or event listener, or a database client that rejects when its connection drops. To log every rejection while you search, add this at the top of your entry file:

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exit(1);
});

Keep the process.exit(1). With a handler in place, Node no longer ends the process, and an app that carries on after a failed operation can be left in a broken state.

Memory: max_memory_restart and the Heap Limit

With max_memory_restart, PM2's worker checks every 30 seconds whether the app uses more memory than the limit, using the figure from the mem column in pm2 list. If so, it restarts the app (a reload in cluster mode) and logs:

2026-10-30T10:12:37: PM2 log: [PM2][WORKER] Process 0 restarted because it exceeds --max-memory-restart value (current_memory=531628032 max_memory_limit=524288000 [octets])

The numbers are bytes: 500M is 500 × 1,024 × 1,024. Restarts at a steady rhythm mean the limit is below what the app needs, or the app leaks and the restarts hide it. pm2 monit shows memory live; if it keeps climbing while traffic stays flat, look for a leak.

Node has its own limit for the V8 heap. When the heap is full, the error log shows a line that starts with FATAL ERROR: and ends in JavaScript heap out of memory, and pm2.log shows via signal [SIGABRT]. If the server has memory to spare, raise the limit with node_args: ['--max-old-space-size=1536']. The value is in MiB, and Node's docs suggest 1536 on a 2 GiB machine to leave room for other processes.

Watch Mode Restart Loops

With watch: true, PM2 restarts the app whenever a file changes in its folder (the exec cwd) or a subfolder. An app that writes there itself, such as logs, uploads, a SQLite database or a cache, restarts on every write, and pm2.log fills with Change detected on path lines that name the file.

In production, turn it off: watch: false in the ecosystem file, or pm2 restart my-app --watch, which toggles it. A plain pm2 stop doesn't end watching, so a stopped app starts again on the next change; pm2 stop my-app --watch does. If you want watch mode, watch only your code:

watch: ['src'],
ignore_watch: ['node_modules', '\\.git', 'logs', 'uploads'],

Your ignore_watch replaces PM2's default, which skips dotfiles and node_modules, so keep node_modules in the list.

Port in Use: EADDRINUSE and EACCES

Error: listen EADDRINUSE: address already in use :::3000

Another process holds the port, and every new start fails within a second, a common road to errored. Find the holder and any second PM2 daemon:

sudo ss -ltnp 'sport = :3000'
ps -eo user,pid,args | grep '[G]od Daemon'

ss names the process and its PID, and ps -o user,pid,args -p <PID> shows its full command. The usual holders:

  • A copy you started by hand with node server.js or npm start, still running. Stop it.
  • A second PM2 daemon. Each one appears as PM2 v7.1.1: God Daemon (/home/app/.pm2). Two lines, for example one for root after a sudo pm2 start, can mean two copies of your app. Run pm2 delete my-app as the user whose copy should go.
  • instances above 1 with exec_mode: 'fork'. All copies but the first fail. Cluster mode shares the port between instances, and PM2 picks it when you set instances without exec_mode.

listen EACCES: permission denied on port 80 or 443 means the app may not bind there: Linux reserves ports below 1024 for privileged processes (CAP_NET_BIND_SERVICE). Keep the app on a port such as 3000 behind nginx; our nginx 502 guide has a server block for that.

Missing Environment Variables After a Deploy

The app ran fine by hand, but after a deploy it crashes on your own "is not set" check or a TypeError about undefined. See what the process received:

pm2 env 0 | grep -E '^(NODE_ENV|PORT|DATABASE_URL):'

pm2 env takes the id from pm2 list and prints every variable, secrets included, so keep the output to yourself. The usual causes:

  1. PM2 kept the old environment. pm2 restart my-app reuses the environment from the first start and prints "Use --update-env to update environment variables". Add --update-env to take your current shell's variables, or run pm2 restart ecosystem.config.js, which applies the file's env (add --env production for env_production). By default, dotenv doesn't overwrite a variable that is already set, so a value PM2 kept also beats an edited .env.
  2. The app looks in the wrong folder. dotenv reads .env from the current working directory, and without a cwd option PM2 starts the app in the folder you ran pm2 start from. Check exec cwd, then set cwd or give dotenv an absolute path: require('dotenv').config({ path: '/home/app/my-app/.env' }).
  3. The release has no .env. dotenv's README advises against committing the file, so a fresh clone starts without one. Keep it outside the release folders, as our Next.js deploy guide does.

The Kernel's OOM Killer

When the VPS runs out of memory, the kernel kills a process and logs it. PM2 only sees SIGKILL and starts the app again, often until the next kill:

sudo journalctl -k | grep -i 'killed process'
free -h

journalctl -k shows kernel messages from the current boot. Out of memory: Killed process with your app's process name, at the time of a SIGKILL in pm2.log, settles it; Memory cgroup out of memory means a cgroup limit was hit, not the server's memory. In free -h, available estimates what is left for new programs without swapping.

Use less memory or get more. Set max_memory_restart below the point where the kernel steps in, so PM2 restarts the app cleanly first, run fewer cluster instances (each is a full copy of the app) and cap the heap. Our guide to how much RAM a VPS needs covers measuring and swap. Space-Node's VPS plans come with dedicated RAM, and you can upgrade to a bigger plan from the client area.

If It Still Fails

Run the app the way PM2 does, in the foreground, so the error lands in your terminal:

pm2 stop my-app
cd /home/app/my-app
NODE_ENV=production PORT=3000 node server.js

Take the folder and arguments from exec cwd, interpreter args and script args in pm2 show. If it runs fine by hand, compare the environment. Press Ctrl+C, then run pm2 restart my-app to hand the app back to PM2.

FAQ

What does errored mean in PM2? PM2 stopped restarting the app after 16 unstable restarts (max_restarts): runs shorter than min_uptime, 1 second by default, soon after a start. It will not try again by itself. Fix the error, then run pm2 restart my-app.

Does pm2 restart reset the restart counter? No. On a running app it adds one. Either way, it sets the unstable restarts back to zero and opens a new window for them, 16 seconds with the defaults. pm2 reset my-app sets both counters to zero.

Why does PM2 restart my script after it finished? PM2 restarts an app after every exit, exit code 0 included. Set autorestart: false for a script that should run once, or stop_exit_codes: [0] to restart only after a failure.

How high should I set max_memory_restart? Above what the app uses at its busiest (the mem column in pm2 list), and low enough that the server still has free memory when it triggers. A restart every few minutes means the limit is too low or the app leaks.

Why does my site return a 502 while the app restarts? Between the exit and the new start, nothing listens on the port, and nginx answers requests in that gap with 502 Bad Gateway. Our nginx 502 guide covers this and the gaps during deploys.

Sources

All opened on 30 October 2026: