The Node.js Process Lifecycle
Running a Node.js service in production is not the same skill as writing one.
Search across all documentation pages
Running a Node.js service in production is not the same skill as writing one.
A script that works perfectly under node app.js on a laptop can still fail an orchestrator's rollout, drop in-flight requests on every deploy, or hang forever on shutdown - because none of those failures live in application logic. They live in the process lifecycle: the sequence a Node process moves through from the moment a supervisor starts it to the moment it exits, and the signals and checks that sequence depends on.
This page is the mental model underneath everything else in this section. Runtime Ops Basics walks through working examples of health endpoints and shutdown handlers; Graceful Shutdown, Zero-Downtime Deploys, and PM2 & systemd each go deep on one stage. Here, the goal is the shape of the whole lifecycle and why an orchestrator, not your application code, is the one driving it.
SIGTERM or a supervisor with too short a grace period breaks the contract from either side.A Node process is, at the operating-system level, nothing special - it is one OS process, with a PID, that the OS can send signals to and that eventually exits with a status code.
What makes "runtime ops" its own discipline is that in production this process is never running alone. It sits inside a supervisor - systemd, PM2, a Kubernetes kubelet, a serverless platform's control plane - whose entire job is deciding when to start it, when to route traffic to it, when to stop it, and when to restart it if it dies unexpectedly.
That supervisor relationship reframes what "stopping a server" means. On a laptop, stopping means Ctrl+C and the process is gone. In production, stopping is a negotiation: the supervisor asks the process to stop, and the process gets a window of time to finish what it's doing before the supervisor forces the issue.
A useful analogy is a shift change at a restaurant counter. The outgoing server does not just walk away mid-order the moment their shift ends - they stop taking new orders, finish the tickets already in hand, and only then clock out. The manager (the supervisor) sets a deadline for that handoff; if the server is still there when the deadline passes, the manager steps in and closes the register anyway. A Node process's shutdown sequence follows the same shape: stop accepting new work, finish in-flight work, then exit - all within a grace period someone else controls.
The lifecycle has five stages, and each one is driven by a different signal or check:
start ──▶ ready ──▶ serving ──▶ draining ──▶ exit
│ │ │ │ │
process ready- normal SIGTERM process
boots probe request received, code 0
passes handling stop new (or SIGKILL
work, finish after grace
in-flight period)
Start is process boot - module loading, config parsing, opening database pools. Nothing should serve traffic yet, because dependencies may not be connected.
Ready is the point where a readiness probe (an HTTP endpoint like /ready, or an equivalent check) starts returning success. This is a different question from liveness: liveness asks "is the process still running and not deadlocked," while readiness asks "should traffic be routed to it right now." A process can be alive but not ready - for example, still opening a database connection pool - and an orchestrator that conflates the two will route requests to a process that isn't prepared to handle them.
Serving is steady-state request handling, the stage a process spends most of its life in.
Draining begins the moment the process receives SIGTERM - the OS-level "please stop" signal every supervisor sends before a stop, scale-down, or deploy. This is the stage most runtime-ops bugs live in, because it requires the process to do several things in order: flip the readiness probe to failing (so the load balancer stops sending new requests), stop accepting new connections, let in-flight requests finish, close resources like database pools cleanly, and only then exit. Graceful Shutdown covers that sequence in full.
let ready = true;
process.on('SIGTERM', async () => {
ready = false; // readiness probe now fails - LB stops routing here
server.close(async () => { // stop accepting new connections; wait for in-flight
await dbPool.end(); // close resources only after requests finish
process.exit(0);
});
});The reason this order matters is the event loop itself: Node's loop keeps a process alive exactly as long as it has pending timers, open handles, or unfinished work (see The Node.js Event Loop). Calling server.close() does not kill in-flight connections - it stops the server from accepting new ones and lets the loop keep running until existing requests complete naturally, which is precisely the drain behavior a supervisor expects.
Exit is the final stage - either a clean process.exit(0) once draining finishes, or a SIGKILL from the supervisor if the process didn't finish within its grace period. SIGKILL cannot be caught or delayed; it is the supervisor's blunt-force guarantee that a stuck process won't hang a deploy forever.
The lifecycle model looks simple in isolation, but production adds real edge cases around it.
Uncaught exceptions and unhandled rejections don't fit cleanly into any of the five stages - they're supposed to be exceptional. Node's own guidance is that once an uncaughtException fires, the process's internal state may be corrupted in ways that are unsafe to keep serving from; the safe pattern is to log the error, attempt a fast clean shutdown, and let the supervisor restart a fresh process, rather than trying to "catch and continue" indefinitely.
Multiple stages of supervision often stack. A container might run under PM2 for process-level restarts, inside a pod managed by Kubernetes for scheduling and scaling, behind a load balancer that owns its own health-check cadence - each layer asking a version of "is this instance ready" independently, and each with its own timeout that has to be longer than the one beneath it or shutdowns race each other.
The grace period is a real operational number, not a default to leave alone. Too short, and slow requests get killed mid-flight during every deploy; too long, and a genuinely stuck process blocks a rollout or scale-down for minutes. Getting this number right requires knowing your actual p99 request duration under load, not guessing.
Observability is the feedback loop that makes this whole model trustworthy. Structured logs to stdout, request IDs, and process-level metrics (event-loop lag, memory, open handles) are what let an operator confirm a deploy actually drained cleanly instead of assuming it did.
| Supervisor | Strength | Weakness | Best Fit |
|---|---|---|---|
| systemd | Native to the VM, no extra runtime, mature restart policies | No built-in health-check routing or scaling | Single-service VMs, simple bare-metal deploys |
| PM2 | Zero-downtime reload built in, easy cluster mode on one box | Not a real orchestrator - no multi-host scheduling | Small teams on a handful of VMs, no Kubernetes |
| Kubernetes | Liveness/readiness probes, rolling deploys, autoscaling, multi-host | Real operational complexity; a lot to learn and run | Multi-service fleets that need scaling and self-healing |
| Serverless (e.g. Lambda) | No process lifecycle to manage at all - platform owns start/stop | Cold starts, execution time limits, different mental model entirely | Bursty or infrequent workloads, event-driven functions |
PM2 & systemd and Zero-Downtime Deploys turn this comparison into concrete configuration; On-Call Runbook Starters covers what to check when a lifecycle transition fails in production.
SIGTERM means the process is being killed." It means the supervisor is asking the process to stop - the process is expected to finish in-flight work first; SIGKILL, not SIGTERM, is the forceful one and cannot be intercepted.server.close() drops any request still in flight." It stops accepting new connections; requests already being handled complete normally because the event loop stays alive until they do.The sequence a process moves through under supervision - start, ready, serving, draining, exit - where an external supervisor, not the process itself, decides when most transitions should begin.
On a laptop, nothing else depends on the process's exact stop timing. In production, a load balancer is routing live traffic to it, a deploy is trying to replace it without dropping requests, and an orchestrator needs a reliable signal for when it's safe to route traffic there at all - none of which exists outside a supervised environment.
Liveness asks "should this process be restarted because something is wrong," while readiness asks "should traffic be routed here right now." A process opening a database pool at startup is alive but not yet ready; a process deadlocked on a lock might still respond to a liveness check while being unable to serve real requests, which is why some teams add a stricter liveness check too.
The supervisor (systemd, Kubernetes, PM2) sends the OS-level SIGTERM signal to the process's PID. Node exposes this as a process.on('SIGTERM', ...) event you can handle; without a handler, Node's default behavior is to exit immediately, which skips any draining.
server.close() stops the server from accepting new connections but does not force existing ones closed - the event loop keeps running because those in-flight requests are still pending work, and the process only exits once they finish and every open handle is released.
The supervisor's grace period expires and it sends SIGKILL, which terminates the process immediately and cannot be caught, delayed, or cleaned up after - any request still in flight at that moment is dropped.
Generally no - an uncaught exception means an error escaped every handler in the call stack, which can leave internal state (open connections, half-updated in-memory data) in an unknown condition. The safer pattern is to log it, shut down cleanly, and let the supervisor start a fresh process.
The signal-handling code in your app is the same either way - it's the supervisor's behavior around it that differs. Kubernetes adds a readiness probe endpoint the app must expose and a defined grace period (terminationGracePeriodSeconds) the cluster enforces, on top of the same SIGTERM-then-SIGKILL pattern systemd and PM2 also use.
Usually because the readiness probe doesn't flip to failing early enough in the drain sequence, so the load balancer keeps sending new requests to an instance that's already begun shutting down - the fix is ordering: fail readiness first, then stop accepting connections, then let in-flight work finish.
No - a crash is an unplanned exit, usually from an uncaught error or being killed without warning, while a graceful shutdown is a planned, signal-driven sequence the process controls the pace of (within its grace period). Runtime-ops tooling assumes crashes are exceptional and shutdowns are routine.
The event loop only exits once it has no pending timers, no open handles (like a listening server or open socket), and no queued I/O - every one of those is effectively a vote to keep the process alive, which is why leaked handles (an unclosed database connection, for instance) can prevent a process from ever exiting cleanly.
Only loosely - a serverless platform owns the start/stop lifecycle entirely and typically gives you no signal-handling window at all, so the "draining" stage mostly doesn't apply. It's still worth knowing the model, because moving a long-lived service to serverless (or back) means reasoning about a genuinely different lifecycle, not just a different deploy target.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS) and TypeScript 5.6+.
Reviewed by Chris St. John·Last updated Jul 15, 2026