Photo by Scott Rodgerson on Unsplash
Connection Draining: Graceful Shutdown in Load Balancers
When you deploy a new version of your application or scale down infrastructure, servers need to stop accepting new work while finishing what they’ve already started. Connection draining is the mechanism that makes this possible without dropping active requests or cutting off users mid-operation.
The Problem: Abrupt Termination
Without connection draining, shutting down a server looks catastrophic from the client’s perspective. A load balancer sends requests to a backend instance right up until the moment it disappears. Any request in flight when the process dies returns an error. WebSocket connections break. File uploads fail halfway through. Database transactions abort.
The naive solution—just wait a few seconds before killing the process—doesn’t work reliably. Some requests finish in milliseconds, others take tens of seconds. A fixed delay either wastes time or cuts off legitimate work.
How Connection Draining Works
Connection draining introduces a shutdown phase where the server stops accepting new work but continues processing existing requests. The load balancer removes the instance from its pool of healthy backends immediately, so no new requests arrive. Meanwhile, the server finishes handling whatever it’s already committed to.
The process has three stages. First, the load balancer marks the backend as draining and stops routing new requests to it. Second, the backend continues processing active connections and in-flight requests. Third, once all work completes or a timeout expires, the process shuts down cleanly.
Most production load balancers—AWS ELB, GCP Load Balancing, HAProxy, Envoy—implement this pattern with configurable drain timeouts. Typical values range from 30 seconds for stateless APIs to several minutes for long-polling connections or batch processing endpoints.
Implementation Patterns
For HTTP APIs, connection draining means finishing any request that has already started. A server stops listening on its port to reject new connections, but keeps existing sockets open. It processes the request, writes the response, and closes the connection normally.
For persistent connections like WebSockets or gRPC streams, the server signals clients to reconnect elsewhere. WebSocket servers send a close frame with a “going away” status code. gRPC servers return a GOAWAY frame directing clients to other instances.
For queue workers and background jobs, the pattern is similar: stop pulling new work from the queue, finish the current batch, then exit. Many job frameworks provide shutdown hooks that wait for active tasks before terminating the process.
Coordination with Health Checks
Connection draining depends on tight coordination between health checks and shutdown signals. When a server receives a shutdown signal (SIGTERM), it immediately starts failing its health check endpoint while continuing to serve in-flight requests. The load balancer detects the failing health check within one or two probe intervals and removes the instance from rotation.
The drain timeout must be longer than both the health check interval and the maximum expected request duration. If health checks run every 10 seconds and requests can take up to 60 seconds, the drain timeout should be at least 70 seconds to guarantee zero dropped requests.
Edge Cases and Limits
Connection draining can’t solve every problem. Clients that retry failed requests aggressively can overwhelm remaining backends during a deployment. Very long-lived connections eventually need to be terminated, even if work is still in progress. Some load balancers impose maximum drain timeouts—AWS Application Load Balancer caps it at 3600 seconds.
For true zero-downtime deployments, connection draining works best alongside other patterns: gradual rollouts that deploy to a small percentage of instances first, automated rollback on error rate spikes, and client-side retry logic that handles transient failures gracefully.
The mechanism is simple—finish your work before leaving—but the coordination across load balancers, health checks, and application shutdown hooks is what makes modern infrastructure resilient to constant change.