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.
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
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
- 01
server.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
timeoutMsbelow Kubernetes'terminationGracePeriodSeconds(30s by default), or the kubelet sendsSIGKILLbefore your cleanup runs. - 03In Kubernetes, endpoints are removed asynchronously. A short
preStopsleep (5–10s) avoids receiving new requests afterSIGTERM. - 04Run Node as PID 1 directly (
CMD ["node", "server.js"]) or viatini. Wrapping it innpm startor a shell can swallow the signal.
Need this wired into a real system?
We build and harden production software end to end.
More code helpers
Redis sliding-window rate limiter
Limit requests per user, API key or IP across every instance of your service. A single Lua script keeps the check-and-increment atomic, so concurrent requests can't slip past the limit.
Verify webhook HMAC signatures
Confirm that an incoming webhook was sent by the provider and wasn't replayed. Uses a constant-time comparison and a timestamp tolerance, the same scheme Stripe and many others use.
Stream LLM responses from a Next.js route handler
Send tokens to the browser as the model produces them instead of making users wait for the full completion. Uses a plain ReadableStream, so it works with any client that can read a fetch body.