Process Management
Node.js Intermediate
Chapter 8 of 8 · Process Management
Process Management
Node runs on a single thread and will exit the moment an unhandled error crashes it, or when you restart the server to deploy new code. Process management answers three questions: How do you keep the server running when it crashes? How do you use all the CPU cores on the machine? And how do you shut down cleanly without cutting off requests mid-flight? This chapter covers the tools and patterns that answer all three.
Unix Signals — The Language of Process Control
Signals are messages the OS sends to a running process. They're how Ctrl+C stops your server, how PM2 tells your app to restart, and how you trigger a graceful shutdown from a deployment pipeline.
| Signal | Number | Default action | Catchable? | Common use |
|---|---|---|---|---|
SIGTERM |
15 | Terminate | Yes | Polite shutdown request — the standard signal for stopping a service gracefully |
SIGINT |
2 | Terminate | Yes | Ctrl+C in the terminal — treat like SIGTERM in production code |
SIGHUP |
1 | Terminate | Yes | Traditionally "terminal closed" — often repurposed for config reload without restart |
SIGUSR1 |
10 | Terminate | Yes | Reserved by Node for the debugger; PM2 uses it for graceful reload |
SIGKILL |
9 | Kill immediately | No | Force-kill when SIGTERM is ignored — no cleanup possible |
Graceful Shutdown
An abrupt exit drops in-flight requests, half-written database rows, and open connections. Graceful shutdown means: stop accepting new work, finish what's already in progress, close resources, then exit.
Handling Uncaught Errors
Two process-level events catch errors that escaped all try/catch and .catch() handlers. Without them, Node prints a warning (or crashes, depending on the version) and leaves your app in an unknown state.
💡 Exit After uncaughtException — Always
After an uncaught exception, the process is in an unknown state: open file descriptors, partially executed callbacks, corrupted in-memory data. The only safe action is to log the error and exit. Let your process manager (PM2, systemd, Docker) restart the process fresh. Never swallow the error and continue.
The Cluster Module — Using All CPU Cores
Node is single-threaded, so a 16-core machine runs one Node process on one core by default. The built-in cluster module spawns one worker process per CPU, with the primary process distributing incoming connections across them.
💡 Cluster vs Worker Threads
Cluster spawns separate OS processes (like child processes from Chapter 3), each with their own memory — no shared state. This means session data, in-memory caches, or rate-limiter counters can't live in application memory; they need an external store (Redis, a database). Worker Threads (Chapter 4) share memory within one process and are suited to CPU tasks, not to handling HTTP connections across cores.
PM2 — Production Process Manager
Building cluster logic yourself works, but PM2 gives you restarts, clustering, log management, startup scripts, and monitoring out of the box with a single command.
ecosystem.config.js — declarative PM2 configuration
PM2 Zero-Downtime Reload
In cluster mode, pm2 reload restarts workers one at a time. Each worker receives SIGINT, finishes its in-flight requests, closes, and is replaced — leaving the other workers serving traffic throughout. There's no moment where the port is unbound.
⚠️ Common Process Management Mistakes
Not handling SIGTERM — without a handler, Node exits immediately when PM2 sends SIGTERM. In-flight requests are dropped, database connections are abandoned, and writes may be incomplete.
Sharing in-memory state in cluster mode — each worker is a separate process. A rate limiter, session store, or job queue living in JavaScript memory is invisible to the other workers. Use Redis or a database for shared state.
Using pm2 restart instead of pm2 reload — restart stops all workers simultaneously (brief downtime); reload rotates them one by one (zero downtime). In production, always reload.
Catching uncaughtException and continuing — after an uncaught exception the process heap may be corrupted. Log and exit; let PM2 restart a fresh process immediately.
Coding Challenges
Challenge 1 — Graceful Shutdown with Timeout
Write a createGracefulShutdown(server, options) function that registers SIGTERM and SIGINT handlers. On signal: (1) stop accepting new connections, (2) wait for in-flight requests with a configurable drainMs timeout (default 10 s), (3) close a provided pool if given, (4) call an optional onShutdown async hook, then exit 0. If the drain timeout expires first, log a warning and exit 1. The function must be idempotent — receiving two signals doesn't run shutdown twice.
Challenge 2 — Cluster with Health Reporting
Build a clustered HTTP server (one worker per CPU) where: (a) each worker responds to GET /health with its own PID and request count, (b) the primary process tracks how many times each worker has been respawned and refuses to respawn a worker that has crashed more than 5 times in 60 seconds (remove it instead and log a warning), and (c) workers send their request count to the primary every 5 seconds via IPC so the primary can log aggregate stats.
Challenge 3 — SIGHUP Config Reload
Write a server that loads its configuration from a JSON file at startup. When it receives SIGHUP, it re-reads the file and applies any changed values (port changes are ignored at runtime — log a notice instead). The config reload must be atomic: the old config stays active until the new one is fully parsed and validated. If the new file has invalid JSON or fails validation, log the error and keep the old config. Demonstrate by changing a value in the file and sending kill -HUP <pid>.