Deploys should not create a small, random packet-loss window. When a container receives SIGTERM, it needs to stop becoming eligible for new work, finish what it already owns, and exit before the platform's grace period expires.
Short answer: make your actual application process receive SIGTERM, flip readiness to false, close listeners, drain work with a deadline, then close database and queue connections. SIGKILL cannot be handled, so anything left after the deadline is simply abandoned.
A Node HTTP sequence
let draining = false;
process.on("SIGTERM", async () => {
draining = true; // readiness endpoint now returns 503
server.close(); // no new HTTP connections
await Promise.race([drainJobs(), timeout(25_000)]);
await pool.end();
process.exit(0);
});
| Order | Why it matters |
|---|---|
| Fail readiness | Load balancers stop selecting this instance |
| Stop accepting connections | New requests do not extend the drain forever |
| Finish or hand off jobs | Avoid duplicate or half-complete work |
| Close dependencies | Releases database and broker resources |
| Exit before grace period | Avoids an uncatchable kill |
Do not put your app behind
sh -cas PID 1 and assume signals arrive where you expect. Use an exec-form DockerCMD, or an init process that forwards signals correctly.
Test the unhappy path
Run a request that takes ten seconds, send SIGTERM halfway through, and inspect the client result. Then repeat with a job consumer and a database transaction. A shutdown hook that only logs “received SIGTERM” has tested nothing.
Graceful shutdown is backpressure at deployment time: communicate that you are leaving, stop accepting responsibility, then leave cleanly.
Cover photo by Wolfgang Weiser on Pexels.
