import http from 'node:http';
export function setupGracefulShutdown(server: http.Server, opts?: { timeoutMs?: number }) {
const timeoutMs = opts?.timeoutMs ?? 10_000;
const sockets = new Set<import('node:net').Socket>();
let isShuttingDown = false;
server.on('connection', (socket) => {
sockets.add(socket);
socket.on('close', () => sockets.delete(socket));
});
const shutdown = (signal: string) => {
if (isShuttingDown) return;
isShuttingDown = true;
// Stop accepting new connections.
server.close(() => {
// eslint-disable-next-line no-console
console.log(`[shutdown] closed server (${signal})`);
});
// Force-close lingering sockets.
setTimeout(() => {
for (const s of sockets) s.destroy();
}, timeoutMs).unref();
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
return {
get isShuttingDown() {
return isShuttingDown;
}
};
}
Deploys got a lot calmer once I treated SIGTERM as a first-class signal instead of an afterthought. In Kubernetes (and most PaaS platforms), you’re expected to stop accepting new requests quickly while finishing in-flight work. The worst failure mode I’ve seen is a container that keeps taking traffic while its dependencies are already draining, which turns into timeouts and noisy retries. I track open sockets so I can force-close them after a deadline, and I flip readiness to unhealthy as soon as shutdown begins. It’s not glamorous, but server.close() + a hard timeout gives you predictable behavior during rolling deploys and autoscaling.