Graceful shutdown and connection draining: an interactive rolling deploy
Servers are stopped all the time: every deploy, every scale-in, every node that is drained for maintenance. Each stop can fail requests, by cutting off the ones in progress or by receiving new ones after the server has stopped listening. Graceful shutdown avoids both: take the server out of the load balancer, wait until that has reached every router, stop accepting, finish what is in flight, then exit.
The playground runs a rolling deploy of four servers three ways and marks every failed request. Add long uploads and change how long a server may take to drain.
Four servers behind a load balancer take 100 requests a second, each under 2 seconds, and are replaced one at a time; a new server takes 8 seconds to boot. Killing the servers cuts off 69 requests in progress over the deploy, and the load balancer keeps sending each dead server traffic until its health checks fail: 800 more. SIGTERM alone lets each server finish what it has, but it stops accepting before the load balancer stops sending: 303 requests refused. Leaving the load balancer first, waiting for that to take effect, and only then draining fails nothing, at the cost of a slower deploy: 60 s against 40 s.
- Cut off mid-request
- 69
- Sent to a server that had stopped
- 800
- Deploy took
- 40 s
- Cut off mid-request
- 0
- Sent to a server that had stopped
- 303
- Deploy took
- 45 s
- Cut off mid-request
- 0
- Sent to a server that had stopped
- 0
- Deploy took
- 60 s
Two ways a stop fails requests
- Cutting off work in progress. Killing the process ends every request it is handling. In the playground that is 69 requests across the deploy, plus 800 more sent to dead servers before the load balancer’s health checks noticed.
- The deregistration race. Even a process that handles SIGTERM correctly, stopping its listener and finishing its requests, fails requests: the load balancer learns that the server is leaving a few seconds later and keeps sending traffic meanwhile. In Kubernetes the pod receives SIGTERM at the same time as it is removed from the service’s endpoints, and proxies and ingress controllers update after a delay. That costs 303 refused requests here.
The recipe
- Leave the load balancer first. Mark the server not ready, or deregister it, before it stops accepting.
- Wait for that to propagate, a few seconds, with a preStop hook that sleeps, while the server keeps serving whatever still arrives.
- Then stop accepting and drain: close the listener, tell keep-alive clients to reconnect elsewhere, finish in-flight requests, flush logs and metrics, and exit.
- Bound the drain. Kubernetes kills the pod after
terminationGracePeriodSeconds, 30 by default, so it must cover the wait plus the longest request you want to protect.
In the playground this fails nothing for quick API requests, at the price of a slower deploy, 60 seconds instead of 40. Long requests stretch it further: with uploads of up to 40 seconds and a 30-second drain, 3 uploads are still cut off, and raising the limit to 60 seconds saves them while the deploy takes over three minutes. For really long-lived work, such as websockets or large uploads, make clients reconnect or resume rather than waiting for them.
Graceful shutdown in Kubernetes and Go
# Kubernetes: leave the load balancer, wait, then let the app drain
spec:
terminationGracePeriodSeconds: 45 # preStop plus the longest drain, with margin
containers:
- name: api
lifecycle:
preStop:
exec: { command: ["sleep", "10"] } # let endpoint removal reach every proxy
readinessProbe:
httpGet: { path: /ready, port: 8080 }
// Go: on SIGTERM, stop accepting, finish in-flight requests, then exit
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer stop()
go srv.ListenAndServe()
<-ctx.Done()
drainCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(drainCtx) // closes listeners and idle connections, waits for active requestsIn a system design interview
When you say “zero-downtime deploys”, say how: “Pods roll one at a time. On termination a pod fails its readiness check and its preStop hook sleeps 10 seconds so every proxy stops routing to it; then the server stops accepting, finishes in-flight requests within 30 seconds, and exits. Websocket clients get a close frame and reconnect to another pod, and long uploads are resumable.” Expect the follow-ups: background jobs that are mid-task at shutdown, and connection pools to the database during the deploy.
Read the guide to deployment strategies or Kubernetes basics, see canary deployments, or watch a strong candidate design a payment system.
Questions people ask
What is graceful shutdown?
Stopping a server without failing requests: it is removed from the load balancer first, keeps serving until that change has propagated, then stops accepting new connections, finishes the requests in progress within a deadline, and exits.
What is connection draining?
Letting a server finish its in-flight requests, and stop receiving new ones, before it is removed or stopped. Load balancers call it connection draining or deregistration delay; servers implement it by closing their listener and waiting for active requests to complete.
Why do requests fail during Kubernetes rolling updates?
Because a terminating pod gets SIGTERM at the same moment it is removed from the service endpoints, and proxies and ingress controllers learn of the removal a few seconds later. If the app stops accepting immediately, requests routed in that window fail. A preStop hook that sleeps for a few seconds closes the gap.
What should an application do on SIGTERM?
Stop accepting new connections, tell keep-alive clients to reconnect elsewhere, finish in-flight requests, flush buffers such as logs and metrics, release resources, and exit before the platform’s grace period ends. In Go, http.Server.Shutdown does most of this.
What is terminationGracePeriodSeconds?
How long Kubernetes waits after starting to terminate a pod, including its preStop hook, before killing it with SIGKILL. The default is 30 seconds; it must cover the propagation wait plus the longest request you want to let finish.
How do you handle long-lived connections during a deploy?
Waiting for them can make deploys very slow, so instead close them politely and let clients reconnect to another server: send a close frame on websockets, make uploads resumable, and checkpoint long jobs so another worker can continue them.
Is graceful shutdown the same as zero-downtime deployment?
It is one part of it. Zero-downtime deploys also need enough capacity during the rollout, readiness checks so new servers get traffic only when ready, backward-compatible changes so old and new versions can run together, and graceful shutdown of the old servers.