All code helpers
BackendTypeScript

Graceful shutdown for Node.js HTTP servers

Finish in-flight requests and close database connections before the process exits, so deploys and autoscaling don't drop requests or leave half-written work.

#nodejs#kubernetes#reliability#http
lib/graceful-shutdown.ts
import type { Server } from "node:http";

type Cleanup = () => Promise<void> | void;

type Options = {
  /** Force exit after this long; keep it below the orchestrator's grace period */
  timeoutMs?: number;
  /** Run after the server stops accepting requests, e.g. close DB pools */
  cleanup?: Cleanup[];
};

export function enableGracefulShutdown(
  server: Server,
  { timeoutMs = 25_000, cleanup = [] }: Options = {},
) {
  let shuttingDown = false;

  const shutdown = async (signal: NodeJS.Signals) => {
    if (shuttingDown) return;
    shuttingDown = true;
    console.info(`${signal} received, draining connections`);

    // Hard deadline so one stuck request can't block the deploy forever
    const timer = setTimeout(() => {
      console.error("Graceful shutdown timed out, forcing exit");
      process.exit(1);
    }, timeoutMs);
    timer.unref();

    try {
      // Stop accepting connections; resolves once in-flight requests finish
      const closed = new Promise<void>((resolve, reject) =>
        server.close((err) => (err ? reject(err) : resolve())),
      );
      server.closeIdleConnections(); // drop idle keep-alive sockets now
      await closed;

      for (const fn of cleanup) await fn();
      process.exit(0);
    } catch (err) {
      console.error("Error during shutdown", err);
      process.exit(1);
    }
  };

  process.once("SIGTERM", shutdown);
  process.once("SIGINT", shutdown);

  return { isShuttingDown: () => shuttingDown };
}

Usage

TypeScript
import express from "express";
import { enableGracefulShutdown } from "./lib/graceful-shutdown";
import { pool } from "./db";

const app = express();
const server = app.listen(3000);

const { isShuttingDown } = enableGracefulShutdown(server, {
  cleanup: [() => pool.end()],
});

// Fail readiness first so the load balancer stops sending traffic
app.get("/readyz", (_req, res) => {
  res.status(isShuttingDown() ? 503 : 200).end();
});

How it works & gotchas

  • 01server.close() stops new connections but waits for open ones. Keep-alive sockets would hold it open, which is why idle connections are closed explicitly.
  • 02Keep timeoutMs below Kubernetes' terminationGracePeriodSeconds (30s by default), or the kubelet sends SIGKILL before your cleanup runs.
  • 03In Kubernetes, endpoints are removed asynchronously. A short preStop sleep (5–10s) avoids receiving new requests after SIGTERM.
  • 04Run Node as PID 1 directly (CMD ["node", "server.js"]) or via tini. Wrapping it in npm start or a shell can swallow the signal.

Need this wired into a real system?

We build and harden production software end to end.

Talk to an engineer