typescript 103 lines · 3 tabs

Graceful shutdown for Node HTTP servers

Shared by codesnips Jan 2026
3 tabs
import express, { type Express, type Request, type Response } from 'express';

export function buildApp(isShuttingDown: () => boolean): Express {
  const app = express();
  app.disable('x-powered-by');

  app.get('/healthz', (_req: Request, res: Response) => {
    res.status(200).json({ status: 'alive' });
  });

  app.get('/readyz', (_req: Request, res: Response) => {
    if (isShuttingDown()) {
      res.status(503).json({ status: 'draining' });
      return;
    }
    res.status(200).json({ status: 'ready' });
  });

  app.get('/work', async (_req: Request, res: Response) => {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    res.json({ done: true });
  });

  return app;
}
3 files · typescript Explain with highlit

A process that handles SIGTERM by calling process.exit() immediately will sever connections mid-response, drop in-flight work, and can corrupt clients that expected a clean reply. Graceful shutdown instead stops accepting new work, lets outstanding requests finish, and only then exits. This snippet shows the pattern as three collaborating files: a shutdown coordinator, its wiring into an HTTP server, and the Express app that exposes health probes.

In gracefulShutdown.ts, createShutdownManager closes over the http.Server and returns a shutdown function bound to signal handlers. Calling server.close() tells Node to stop accepting new connections and to invoke the callback once every open connection is idle. The catch is that a keep-alive HTTP connection sitting idle will never close on its own, so server.close() can hang forever. To avoid that, the manager tracks live sockets in a Set and, once draining begins, sets Connection: close on the next response of each socket via the beforeExit hook and destroys sockets that are still idle after a timeout. A shuttingDown flag makes the handler idempotent so a second SIGTERM does not start a second teardown.

The forceTimer is the safety valve: if requests refuse to finish within graceMs, the process exits anyway with a non-zero code rather than blocking a container restart indefinitely. unref() on the timer keeps it from holding the event loop open once real shutdown wins the race.

In server.ts, the raw Server is created explicitly so the manager can hook connection events and register SIGTERM/SIGINT handlers. Ordering matters here: signal handlers are attached only after listen succeeds.

In app.ts, two probes cooperate with the drain. Liveness (/healthz) always returns 200, but readiness (/readyz) flips to 503 the instant isShuttingDown() is true. In Kubernetes this is what actually makes the shutdown graceful: the failing readiness probe pulls the pod out of the Service endpoints so no new traffic is routed to it, giving the in-flight requests room to drain before the socket closes. The trade-off is added complexity and the need to tune graceMs against both the load balancer's deregistration delay and the longest legitimate request. Setting the grace window too low reintroduces the abrupt cutoff the pattern was meant to prevent.


Related snips

Share this code

Here's the card — post it anywhere.

Graceful shutdown for Node HTTP servers — share card
Link copied