Every deploy is dropping requests and nobody notices
Von Seaworthy · 16. August 2026
Photo by Dmitrijs Safrans on Unsplash
Most Node apps generated by an AI assistant start the same way:
app.listen(3000, () => {
console.log('Server running on port 3000')
})
That's the whole lifecycle. There's a start. There's no shutdown. When the process receives SIGTERM, which is what Docker, Kubernetes, and most process managers send before killing a container, Node's default behavior is to exit immediately. Whatever requests were in flight at that moment don't get a response. They just stop.
What actually happens during a rolling deploy
A typical deploy looks like this: the orchestrator starts a new container, waits for it to become healthy, then sends SIGTERM to the old one and gives it a grace period, usually somewhere between 10 and 30 seconds, before sending SIGKILL if it hasn't exited. That grace period exists specifically so the old process can finish what it's doing before it goes away.
If there's no handler for SIGTERM, the old process doesn't use that grace period. It just dies, right then, taking every open connection with it. Anyone whose request landed on that instance in the seconds before the signal arrived gets a connection reset instead of a response. On a low-traffic app this might be zero requests most deploys and one or two on a bad day. On anything with real traffic, it's a small but real slice of users hitting an error on every single deploy, indefinitely, because nothing about this gets better on its own.
The fix is listening for the signal and finishing up
const server = app.listen(3000)
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing server')
server.close(() => {
console.log('Server closed, exiting')
process.exit(0)
})
})
server.close() stops accepting new connections but lets existing ones finish. Combined with the orchestrator's grace period, this is usually enough for in-flight requests to complete normally instead of being cut off. If your app holds a database connection pool or a message queue consumer, those need their own shutdown calls in the same handler, ideally awaited before the process actually exits, so a deploy doesn't also drop whatever was mid-transaction.
It's worth setting a hard exit timer too, in case something never resolves:
setTimeout(() => process.exit(1), 10000).unref()
That way a connection that refuses to close doesn't hold the process open past the orchestrator's own patience, which would just result in a SIGKILL anyway, minus the clean log message explaining why.
Why this gets skipped
Graceful shutdown is invisible in local development. You stop the dev server with Ctrl+C, and whether or not SIGTERM is handled, the difference is a process that exits in ten milliseconds versus one that exits in fifty. Nobody notices, because nobody's mid-request when they hit Ctrl+C on their own machine. The cost only shows up under real traffic during a real deploy, which is a scenario that's structurally absent from however an AI assistant validates the code it just wrote.
This is also why it tends to stay missing even in codebases that have been deployed for a while: the failure is a steady drip of errors correlated with deploys, not a single dramatic outage, so it's easy to attribute to noise rather than trace back to a missing signal handler.
Readiness matters as much as the shutdown handler itself
A SIGTERM handler alone doesn't help if the load balancer or ingress in front of your app doesn't stop routing new traffic to the instance that's shutting down. Most orchestrators send SIGTERM and simultaneously start removing the pod or task from service discovery, but there's a window, however brief, where both can be true at once: the process has started closing and the load balancer hasn't caught up yet. A readiness probe that starts failing as soon as the shutdown handler begins, rather than waiting for the process to actually exit, gives the load balancer a clean signal to stop sending new requests during exactly the window where server.close() is trying to drain the old ones.
This is a small addition on top of the handler above, usually just a boolean flag flipped at the top of the SIGTERM callback that a separate /healthz route checks before returning 200. It's easy to skip because the app still works without it, in the sense that requests eventually succeed after a retry. It just means a slower, noisier deploy than the one the shutdown handler was supposed to buy you.
What to check for
If process.on('SIGTERM', ...) doesn't appear anywhere in your app's entry point, deploys are dropping whatever requests happen to be in flight at the moment the old container gets killed. Seaworthy checks for a SIGTERM handler around the server's listen call, since a deploy that silently drops requests tends to get blamed on flaky clients long before anyone looks at the server.
Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.
Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.
Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.