JavaScript/Node.js

Keep Your Node App Alive: PM2 for Crash Recovery and Boot Persistence

A bare node server.js dies with your SSH session or the first crash. How PM2 restarts it, survives reboots, and when to reach past it.

Your deploy just died. You SSH'd into the box, ran node server.js, saw the "listening on 3000" line, closed your laptop, and went to bed. By morning the site was down. Or it stayed up until the first unhandled promise rejection, and then it was down. Either way, you learned the same lesson everyone learns once: a bare node server.js is not a production process. It is a foreground command tied to your terminal, and the moment that terminal closes or the process throws something nobody caught, it is gone.

https://grizzlypeaksoftware.com/articles/api/image/417

Designing Solutions Architecture for Enterprise Integration: A Comprehensive Guide

Designing Solutions Architecture for Enterprise Integration: A Comprehensive Guide

Stop fighting data silos. Build enterprise integration that scales. Real-world patterns, checklists, and case studies. For IT professionals.

Learn More

This is the problem PM2 solves, and it solves the common case well. But PM2 is a tool with opinions and sharp edges, and I have run enough Node in production to know exactly where those edges are. Here is what actually matters.

Why bare node dies

Two separate failure modes get conflated. The first is the SSH session. When you run node server.js over SSH, that process is a child of your shell. Close the connection and the shell gets a SIGHUP, which propagates to the child. Your app dies with your session. People "fix" this with nohup node server.js & or a screen session, and that works right up until the second failure mode: the process crashes on its own. An uncaught exception, an unhandled rejection, an out-of-memory kill from the OS, and Node exits. Nothing restarts it. Now you have a dead app and no terminal to show you why.

You need a supervisor: a long-lived process whose entire job is to keep your app running, restart it when it dies, and survive a reboot. PM2 is that supervisor, packaged for Node developers who do not want to write systemd unit files.

The parts of PM2 that matter

Start with a name. Never run an anonymous process:

pm2 start server.js --name api

Naming matters because every other command references it: pm2 restart api, pm2 logs api, pm2 stop api. Anonymous processes force you to juggle numeric IDs that shift around, and at 2am you will restart the wrong one.

Now the two-step that everyone gets wrong. PM2 running your app in memory does not mean it survives a server reboot. Two separate things have to happen, and they are easy to conflate:

pm2 save
pm2 startup

pm2 save writes the current process list to a dump file. That is the snapshot PM2 resurrects. pm2 startup generates and installs the init script (systemd, on most modern Linux) that launches the PM2 daemon at boot and replays that dump. Run pm2 startup and it prints a sudo env PATH=... pm2 startup systemd -u youruser command that you then have to actually run. People read the output, nod, and move on without executing the printed command. Then they reboot and everything is gone.

The order and the mental model: startup installs the boot hook once, save captures the current state. If you add a new app later, you run pm2 save again to update the snapshot. You do not re-run startup. Get this wrong and your reboot brings back yesterday's process list, or nothing at all.

Auto-restart and its sharp edges

PM2 restarts your app when it crashes. That is the headline feature and it works. The problem is that a supervisor which restarts a broken app instantly, forever, is a crash loop generator. If your app dies on boot because the database is unreachable, PM2 will restart it hundreds of times a second, pin a CPU core, and flood your logs. The restart is not the fix. It is an amplifier.

So you constrain it. In an ecosystem file (more on that below) you set:

  • min_uptime: how long the process must stay up to count as a successful start. Set it to 5s. A process that dies in 200ms was never really up.
  • max_restarts: how many failed restarts inside that window before PM2 gives up and marks the process errored. This is your crash-loop circuit breaker.
  • restart_delay or the built-in exponential backoff: PM2 will space out restarts instead of hammering. exp_backoff_restart_delay: 100 starts at 100ms and backs off, which is the setting I reach for most.
  • max_memory_restart: restart the process if it crosses a memory threshold. This is the pragmatic answer to the slow leak you have not found yet. Set max_memory_restart: '500M' and PM2 recycles the process before the OOM killer does it for you. It is a band-aid, not a cure, and you should still go find the leak. But it keeps you up while you look.

Run it from an ecosystem file, not a pile of flags

The CLI flags are fine for a quick test. They are wrong for production, because six months from now nobody remembers which flags you typed. Put the configuration in version control:

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'api',
      script: './server.js',
      instances: 1,
      exec_mode: 'fork',
      min_uptime: '5s',
      max_restarts: 10,
      exp_backoff_restart_delay: 100,
      max_memory_restart: '500M',
      env: {
        NODE_ENV: 'production',
        PORT: 3000
      }
    }
  ]
};

Then pm2 start ecosystem.config.js. Now your restart policy, environment, and process name live in the repo next to the code they govern. This is the single biggest upgrade most people make. The flags were never the point. The reproducibility is.

Cluster mode: helpful, or a bug multiplier

Set instances: 'max' and exec_mode: 'cluster' and PM2 forks one process per CPU core, load-balancing across them. On a stateless HTTP API that is genuinely free throughput, and it is the right call.

But cluster mode multiplies whatever assumptions you baked into a single process. In-memory sessions? Now each worker has its own, and a user bounces between them and gets logged out at random. A setInterval cron job inside your app? It now runs once per core. A local rate-limiter counter? Useless, each worker counts separately. Cluster mode does not make these bugs. It reveals them, at a volume equal to your core count. Reach for it only when your process is genuinely stateless, and move that shared state to Redis or the database first. Otherwise you have just parallelized your incident.

Logs will fill your disk

PM2 captures stdout and stderr to files under ~/.pm2/logs, and by default they grow without bound. The failure is quiet and then sudden: everything is fine until the disk hits 100 percent, and then nothing writes, the database chokes, and you are debugging a "random" outage that is really a full volume. Install the rotation module on day one:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7

That caps each log at 10MB and keeps a week of history. Do this before you need it, because you will discover you needed it at the worst possible time.

When PM2 is the wrong layer

Here is the honest opinion. PM2 is excellent for keeping one or a few Node apps alive on a single long-lived server, the classic VPS or droplet. That is its home turf and it is very good there.

It is the wrong tool the moment your architecture moves past that. If you are running containers, PM2 inside the container is an anti-pattern: the container runtime already supervises PID 1, and a restart: always policy plus a health check does the crash-recovery job at the layer that owns it. Wrapping PM2 in there just hides crashes from the orchestrator that is supposed to see them. If you are on Kubernetes, the Deployment controller is your supervisor, your restarts, and your boot persistence all at once. And if you want zero extra dependencies on a plain Linux box, a fifteen-line systemd unit with Restart=on-failure and WantedBy=multi-user.target gives you crash recovery and boot persistence with nothing to install, using the init system that is already running everything else on the machine.

PM2 earns its place in the single-server Node deployment where you want process management, log handling, and reloads without writing init scripts. Use it there and it is a joy. Just do not carry it into a container or a cluster out of habit, because in those places it is a second supervisor fighting the one that should win.