Node.js cookbook · Intermediate

Shut down a Node.js server gracefully

Deploy without dropping in-flight requests or leaving database connections and jobs half-done.

Approach

On SIGTERM/SIGINT, mark the instance not-ready (readiness probe fails), stop accepting new connections with server.close(), close idle keep-alive sockets, wait for in-flight requests, then close pools, queues and Redis. Force-exit after a timeout shorter than the orchestrator's grace period.

Solution

import http from "node:http";

let shuttingDown = false;
app.get("/readyz", (_req, res) => res.status(shuttingDown ? 503 : 200).json({ ready: !shuttingDown }));

const server = http.createServer(app);
server.keepAliveTimeout = 65_000;            // > load balancer idle timeout
server.listen(Number(process.env.PORT ?? 3000));

async function shutdown(signal: string) {
  if (shuttingDown) return;
  shuttingDown = true;
  logger.info({ signal }, "shutting down");
  const force = setTimeout(() => { logger.error("forced exit"); process.exit(1); }, 25_000);
  force.unref();

  await new Promise((r) => setTimeout(r, 5_000));     // let the LB notice readiness=503
  server.closeIdleConnections();
  await new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
  await Promise.allSettled([worker.close(), pool.end(), redis.quit()]);
  logger.info("shutdown complete");
  process.exit(0);
}

process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));

Pitfalls

  • Running Node via npm start as PID 1 (npm may not forward signals) — run node directly or use tini.
  • Keep-alive connections preventing server.close() from finishing.
  • Exiting before queues/pools flush.

Variations

  • http-terminator or @godaddy/terminus packages implement this pattern.
  • Fastify: fastify.close() plus close-with-grace.

Further reading: Node.js: server.close()

Reference: https://nodejs.org/api/http.html#serverclosecallback

More Node.js recipes