Early in the year we had an incident where an unhandled promise rejection crashed a client's Node API in the middle of the night, and it stayed down for several hours until a support ticket woke someone up. That is the kind of incident that changes a team's defaults.
What actually happened
The crash itself traced back to a third-party API call whose response shape changed unexpectedly, throwing inside a `.then()` chain with no corresponding `.catch()` anywhere in the call stack. Node's behavior at the time for an unhandled rejection was, depending on version and flags, either a deprecation warning or a hard process exit — in this case it took the whole process down. With nothing watching for the crash and no restart policy configured, the API simply stayed dead until the first customer complaint reached someone who could investigate, which was not a fast path to resolution at two in the morning.
What we changed
We now run every production Node process under PM2 rather than a bare `node server.js` in a screen session or a systemd unit with no restart policy. PM2 restarts a crashed process automatically, keeps logs in a predictable place, and its cluster mode lets us run one process per CPU core on a box without changing a line of application code, giving us both crash recovery and better utilization of multi-core servers in one move.
The specific PM2 features that matter most to us
- Automatic restart on crash, with configurable backoff so a process that is crash-looping does not hammer the CPU with restart attempts in a tight cycle
- Cluster mode, using Node's built-in cluster module under the hood, letting a single PM2-managed app scale across all available CPU cores with a one-line configuration change
- Log rotation via the `pm2-logrotate` module, so logs do not silently fill a disk over months of uptime, something that had actually happened to us once before on an older, non-PM2-managed server
- Zero-downtime reloads for cluster-mode apps, restarting workers one at a time during a deploy rather than dropping all connections at once
- A simple `pm2 monit` view for quickly checking CPU and memory usage across all managed processes on a box without reaching for a separate monitoring tool for a fast sanity check
What PM2 does not solve
PM2 is not a substitute for actually fixing the underlying bug that caused a crash in the first place, and we treat every PM2-triggered restart as something worth investigating rather than something to shrug off just because the process recovered on its own. We added Sentry error tracking around the same time specifically so a crash generates a proper stack trace and an alert, rather than PM2 quietly restarting the process and the underlying bug going unnoticed until it happens again, possibly at a worse time or a higher rate.
It also does not solve availability during a deploy that requires downtime for a genuine reason — a breaking database migration, for instance — and it does not solve availability if the underlying server itself goes down rather than just the process. For that level of resilience we still rely on running multiple servers behind a load balancer for anything sufficiently important, with PM2 handling the process-level resilience on each individual box.
The result
It is a small addition to a deployment, but the failure mode it closes — a silent crash with nobody watching — was costing us real incident-response time before we standardized on it. We have not had a repeat of that original overnight outage on any PM2-managed project since, and even when a crash does happen, which is rare but not impossible, the process is generally back up within a second or two, often before a real user even notices a failed request, which is a categorically different outcome than a service that stays down until a human intervenes.