What does http.Server.Shutdown actually wait for, and what happens when its context expires?
answer
- the unit of accounting is the connection
- a spawned goroutine has no connection
- the slowest request sets the floor
- the deadline bounds you, not them
- returning ctx.Err() closes nothing
basics
~10 sShutdown waits on connections, not goroutines: it closes listeners and idle connections, then waits for active ones to finish. If the context expires it returns that error and stops waiting, leaving those connections open.
solid answer
~50 s`Shutdown` closes the listeners, closes every connection that is currently idle, and then polls until the still-active connections have finished the request they are serving, closing each one as it goes. Its unit of accounting is the connection: a goroutine a handler launched with `go` and never joined is invisible to it, and so is any background worker in the process. Conversely a handler streaming a large report keeps its connection active for the whole transfer, so a single slow download can hold the drain open. When the context deadline fires, `Shutdown` returns `ctx.Err()` and gives up waiting — but that is all it does. The listeners stay closed, the active connections stay open, the handlers keep running, and the request contexts are not cancelled. If you want those requests gone, call `Close` after the error; if you want them to notice, handlers have to honour their own deadlines.
code
go · 11 linesvar jobs sync.WaitGroup
func handle(w http.ResponseWriter, r *http.Request) {
jobs.Add(1)
go func() { defer jobs.Done(); buildReport() }()
w.WriteHeader(http.StatusAccepted) // connection goes idle now
}
// in the shutdown path
_ = srv.Shutdown(ctx) // returns without knowing about buildReport
jobs.Wait() // this is what waits for itgo deeper
Know that Shutdown waits for requests that are already running, and that work your handler kicked off in a separate goroutine is not part of that promise.
Be able to state the three steps and the connection-based accounting, and to say precisely what the context deadline does and does not do when it fires.
Derive the deadline from a request-duration histogram, log Shutdown's return value, and design handlers so an abandoned request is recoverable rather than corrupting.
Frame the deadline as a negotiated cost between deploy speed and cut requests, and decide which endpoint classes are allowed to hold a drain open at all.
## The accounting unit is the connection `Shutdown` is implemented against the set of connections the `http.Server` tracks, and every rule about it follows from that. Step one: close the listeners, so nothing new is accepted. Step two: close every connection that is currently idle — sitting between requests with nobody waiting on it — because those can go with no cost to anyone. Step three: loop, closing each remaining connection as it becomes idle, and return once none are left. Once shutdown has begun the server stops reusing connections for further requests, so an active connection becomes idle exactly once, when its handler returns and the response is written, and is then closed. That design gives you a clean mental test for anything you wonder about: **is it attached to a connection the server is tracking?** If yes, `Shutdown` waits for it. If no, `Shutdown` neither knows nor cares. ## What is therefore *not* waited for **Goroutines your handler spawned.** A handler that writes its 202 response and continues the work in `go generateReport(...)` has returned; its connection is idle; `Shutdown` is satisfied. The report generation is then racing the process exit. If that background work matters, it needs its own `sync.WaitGroup` (or an `errgroup.Group`) that the shutdown path joins after `Shutdown` returns. **Any other component in the process.** A queue consumer, a cache flusher, a metrics reporter: none of these are connections, and `Shutdown` returning tells you nothing about them. Shutdown of the HTTP server is one item in your stop sequence, not the whole of it. ## What is waited for, sometimes for a very long time A connection is active for as long as its handler has not returned. A handler streaming a 200 MB report to a slow client is active for minutes, and `Shutdown` will sit there for the whole transfer unless the context stops it. This is the honest, useful behaviour — that download is exactly what the drain is protecting — but it means the drain's duration is your slowest request, not your median one. ## The deadline does less than people assume This is the single most common misconception on this topic. When the context passed to `Shutdown` expires: - `Shutdown` returns `ctx.Err()`, which is `context.DeadlineExceeded` (or `context.Canceled`). - The still-active connections are **not** closed. - The handlers running on them are **not** interrupted. - Their `*http.Request` contexts are **not** cancelled — a request context is cancelled when the client goes away or when `ServeHTTP` returns, not by a server shutdown. - The listeners remain closed and the server remains permanently shut down. So the deadline bounds *your wait*, not the requests. A program that calls `Shutdown` with a 30-second context, ignores the error and returns from `main` gets the abrupt behaviour anyway when the process exits; a program that ignores the error and then blocks on something else keeps serving those requests indefinitely. The deliberate version is: ```go if err := srv.Shutdown(ctx); err != nil { // we waited long enough; now actually cut them _ = srv.Close() } ``` If instead you want handlers to wind down cooperatively rather than being cut, that has to be built into the handlers: give long operations their own deadlines, check a package-level draining flag before starting a new expensive chunk, or write the response in bounded pieces so an interrupted transfer is resumable. `Shutdown` provides no hook that reaches into a running handler. ## Practical consequences Measure the deadline against your request-duration distribution rather than picking a round number: if the p99 of the download endpoint is 90 seconds, a 10-second drain guarantees you cut real users on every deploy, and a 10-minute drain guarantees deploys crawl. Both are policy choices, and both are made from the same histogram. Remember also that the drain has a floor of roughly "one request duration" no matter how idle the service is, because the check is per connection. And because `Shutdown` returns nil only when the drain truly completed, that return value is worth logging: a service whose shutdown consistently returns `context.DeadlineExceeded` is telling you either that its deadline is too short or that some endpoint never finishes.
- Does Server.Shutdown cancel the context of a request that is still in flight?No. A request's context is cancelled when the client disconnects or when `ServeHTTP` returns — shutdown is not one of its triggers. A streaming handler will not learn from `r.Context()` that a drain is underway; if it needs to know, you have to pass that signal yourself.
- How long does a drain take on a completely idle service?Effectively no time. The listeners close, every connection is idle so all of them are closed at once, and `Shutdown` returns almost immediately. The drain only costs what your in-flight requests cost, which is why a fast service can afford a generous deadline it never uses.
- What does a nil return from Server.Shutdown tell you that the context error does not?That every tracked connection finished its request and was closed before the deadline — the drain genuinely completed. A `context.DeadlineExceeded` return means requests were abandoned mid-flight, which is worth logging and counting: a service that always returns it has either too short a window or an endpoint that never ends.
saying these in an interview costs you the question
- Expects Shutdown to wait for goroutines a handler started
- Thinks the deadline force-closes remaining connections
- Believes shutdown cancels in-flight request contexts
- Assumes a drain is bounded by the median request time
- Ignores Shutdown's return value entirely