A signal is the operating system knocking on your process's door. Docker 514 sends SIGTERM on docker stop, Kubernetes 5,150 before evicting a pod, systemd 142,543 on systemctl restart; each waits a grace period (10 seconds for Docker, 30 for Kubernetes) before SIGKILL, which no program can catch. SIGINT (Ctrl+C), SIGHUP and Windows' SIGBREAK deserve the same handler.
import http from 'node:http'; import process from 'node:process';
const server = http.createServer((req, res) => res.end('ok'));
server.listen(3000, () => console.log('listening, pid', process.pid));
let closing = false;
function shutdown(signal) {
if (closing) return; closing = true;
console.log(`${signal} received; refusing new connections`);
server.close(() => { console.log('drained; exiting cleanly'); process.exit(0); });
server.closeIdleConnections();
setTimeout(() => process.exit(1), 10_000).unref();
}
for (const s of ['SIGINT', 'SIGTERM', 'SIGBREAK']) process.on(s, () => shutdown(s));Output
listening, pid 55324 SIGTERM received; refusing new connections drained; exiting cleanly
server.close() calls back once the last connection ends — but a keep-alive client holds an idle socket for minutes, so closeIdleConnections() is what lets the callback fire. The closing guard matters because operators press Ctrl+C twice, and the unref()ed timer is the backstop: a stuck request exits with code 1 rather than waiting for SIGKILL and losing your logs.