Graceful HTTP Shutdown (connection draining)
Source: Custom
Topics: http.Server, context, connection draining, high availability
Problem
Run an HTTP server that, on a shutdown signal, stops accepting new connections but lets
in-flight requests finish — and forcibly closes anything still running after a drain deadline.
This is what makes a rolling deploy / pod eviction lossless instead of dropping live requests.
Requirements:
Run(ctx context.Context, srv *http.Server, ln net.Listener, drainTimeout time.Duration) error:
- Serve on
ln and block until ctx is cancelled (or the server stops on its own).
- On cancel, stop accepting new connections and wait up to
drainTimeout for in-flight
requests to complete (srv.Shutdown).
- If the drain finishes in time, return
nil.
- If it times out, force-close remaining connections (
srv.Close) and return ErrForced.
- Treat
http.ErrServerClosed as a clean stop, not an error.
var ErrForced = errors.New("graceful: drain timed out, connections forced closed")
func Run(ctx context.Context, srv *http.Server, ln net.Listener, drainTimeout time.Duration) error
Key concepts
srv.Shutdown(ctx): closes listeners immediately, then blocks until all active requests
finish or the passed context expires. It returns the context's error on timeout — that's your
signal to escalate to a forced close.
http.ErrServerClosed: Serve always returns this once Shutdown/Close is called; it's
the normal "we asked it to stop" sentinel, not a failure.
- Drain vs force: draining preserves correctness for active requests; the hard deadline +
srv.Close bounds how long shutdown can take, so a stuck handler can't block a deploy forever.
- Why a listener argument: passing the
net.Listener lets callers (and tests) bind to
127.0.0.1:0 and learn the real port via ln.Addr() before any request races in.
Run
go test -v -race ./challenges/networking/graceful-shutdown/
Sign in to submit your solution.