skip to content

Draining and Graceful Shutdown

Server.Shutdown closes listeners and waits for in-flight requests while ListenAndServe returns http.ErrServerClosed, and Close cuts everything at once. Interviewers ask how a deploy drops no requests.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

6

In Go's net/http, how does Server.Shutdown differ from Server.Close?

level: juniorimportance: must knowfreq 70%

answer

  1. one drains, one cuts
  2. only one of the two takes an argument
  3. a half-finished download either completes or truncates
  4. the bounded wait needs a fallback call
  5. neither leaves the server reusable

basics

~10 s

Server.Shutdown stops accepting new connections, closes idle ones, and waits for in-flight requests to finish; it takes a context that bounds the wait. Server.Close closes every connection immediately, cutting requests off mid-response.

solid answer

~40 s

`Shutdown` is the graceful path. It closes the listeners so nothing new is accepted, closes connections that are sitting idle, and then waits until every remaining connection has finished the request it is serving. It takes a `context.Context`, so the caller decides how long that wait may last; if the deadline passes first, `Shutdown` returns the context's error. `Close` is the abrupt path: it closes the listeners and every tracked connection right away, so a client half way through downloading a report gets a truncated response. In production you normally use both — call `Shutdown` with a bounded context, and if it returns an error, call `Close` to cut whatever is still hanging on. After either call the `Server` is spent: it cannot be reused, and serving on it again returns `http.ErrServerClosed`.

code

go · 9 lines
go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// waits for in-flight report downloads, at most 30s
if err := srv.Shutdown(ctx); err != nil {
	// deadline hit: Shutdown stopped waiting but the
	// connections are still open, so cut them
	_ = srv.Close()
}

go deeper

for a junior

Be ready to name both methods and say in one sentence which one waits. Know that Shutdown takes a context and Close takes nothing, and that a cut connection means a truncated response for the client.

for a middle

Explain the three steps Shutdown performs — close listeners, close idle connections, wait for active ones — and that hitting the context deadline only ends the wait rather than killing anything.

for a senior

Show the escalation pattern from memory and justify the number you put in the context. Be able to say what a cut download actually costs your users and why that decides the deadline.

for a principal

Own this as a service-wide default rather than a per-repo choice: which services may drain longer, what the escalation to Close is allowed to cost, and how you keep that policy consistent across teams.

## The two ways an http.Server can stop An `http.Server` owns two things: one or more listeners (the sockets it accepts new connections on) and a set of live connections, each of which may be idle between requests or actively running a handler. Stopping the server means dealing with both, and `net/http` gives you two methods that make opposite tradeoffs. ### Server.Close — immediate ```go err := srv.Close() ``` `Close` closes the listeners and then closes every connection the server is tracking, whatever state it is in — brand new, actively serving a request, or idle. It returns as soon as that is done, and it returns the error from closing the listeners. There is no waiting and no argument: a client that was three seconds into a two-minute report download simply sees the connection drop, and its HTTP library reports a truncated body or an unexpected EOF. `Close` is the right call for a test that is finished with its server, or as the last resort when a graceful drain has already failed. ### Server.Shutdown — graceful, and bounded by you ```go err := srv.Shutdown(ctx) ``` `Shutdown` runs three steps. First it closes the listeners, so no new connection is accepted (this is also why the serve call returns straight away — see the sentinel error below). Second it closes the connections that are currently idle, because nobody is waiting on them. Third it waits, polling, until every remaining connection has gone idle, and closes each one as it does; connections are not reused for a further request once shutdown has begun, so an active connection goes idle exactly once, when its handler returns and the response is written. The context is the whole point of the method. `Shutdown(context.Background())` waits forever; `Shutdown(ctx)` with a deadline waits at most that long and then returns `ctx.Err()`. What people get wrong is the second half of that sentence: returning the deadline error is all it does. `Shutdown` does not then kill the connections it gave up on, does not cancel the handlers' request contexts, and does not stop the goroutines running them. If you want those requests to actually stop, you have to say so: ```go ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { _ = srv.Close() } ``` That pair — a bounded `Shutdown`, escalating to `Close` — is the shape you should be able to write from memory. ### What both of them share Both mark the server as shut down permanently. A `Server` value is not reusable after either call: calling `ListenAndServe` or `Serve` on it again returns `http.ErrServerClosed` immediately rather than starting to listen. If you need to restart a service in-process, build a new `http.Server`. Both also close the listeners first, which means the serve call that was blocking in another goroutine unblocks with `http.ErrServerClosed` as soon as shutdown *begins*, not when it ends. A program that treats any return from `ListenAndServe` as fatal and exits will therefore kill the process in the middle of the drain it just asked for — the graceful shutdown becomes an abrupt one, with extra steps. And neither method reaches inside your handlers. Shutdown is expressed entirely in terms of connections. A goroutine your handler started and did not wait for is invisible to it; a handler that blocks forever keeps its connection active forever and will keep an unbounded `Shutdown` waiting forever. ### Choosing between them Use `Shutdown` whenever a request being cut off has a cost a user notices: a report download that would have to be restarted, a write that is committed but whose response never arrives, a long poll. Use `Close` when the cost of waiting exceeds the cost of the cut — in tests, in a `defer` in short-lived tooling, and as the escalation after a bounded `Shutdown` has expired. Treating `Close` as "the simple version of Shutdown" is the classic junior mistake; treating `Shutdown` as something that can never take longer than you want, without passing a deadline and following it up with `Close`, is the classic next one.

  • Can you call ListenAndServe again on the same http.Server after Shutdown returns?
    No. Both `Shutdown` and `Close` mark the server permanently shut down, and any later `Serve`, `ListenAndServe` or `ServeTLS` call on that value returns `http.ErrServerClosed` without listening. To serve again, construct a fresh `http.Server`.
  • If Server.Shutdown's context expires, are the remaining requests aborted?
    No. `Shutdown` returns the context's error and stops waiting, but the still-active connections stay open and their handlers keep running. Nothing cancels the request contexts. If you want them gone you must call `Close` yourself, or let the process exit.
  • Which one do you use in a test that spins up a server?
    `Close`, usually in a `defer`, because the test owns every client and does not care about truncating them; it returns immediately and keeps the test fast. `Shutdown` is for production paths where a cut request costs a real user something.

Shutdown is locking the shop door and letting the customers already inside finish paying; Close is turning the lights off and walking out with the tills still open.

saying these in an interview costs you the question

  • Says Close waits for in-flight requests to finish
  • Thinks Shutdown's deadline force-closes the remaining connections
  • Believes the server can be restarted after Shutdown
  • Passes context.Background() to Shutdown and calls it bounded
  • Claims Shutdown cancels each in-flight request's context
open as a page

Why does ListenAndServe return http.ErrServerClosed as soon as Server.Shutdown begins?

level: middleimportance: must knowfreq 58%

basics

~20 s

The first thing Server.Shutdown does is close the listeners, and that is what makes ListenAndServe return. It reports that accepting has stopped, not that the drain is done — the drain ends later, when Shutdown itself returns.

open as a page

What does http.Server.Shutdown actually wait for, and what happens when its context expires?

level: middleimportance: should knowfreq 52%

basics

~10 s

Shutdown 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.

open as a page

Your Go report service's Server.Shutdown never returns and deploys hang. How do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Shutdown returns only when every tracked connection has gone idle, so an unbounded Shutdown blocks forever on one handler that never returns. Capture the goroutine profile with full stacks while it hangs, then bound the context and escalate to Close.

open as a page

How long should Server.Shutdown's drain window be for a Go service deployed many times a day?

level: principalimportance: should knowfreq 28%

basics

~20 s

Derive it from the request-duration distribution, cap it below whatever kill deadline the deploy tooling enforces, and always escalate to Server.Close when it expires. The number is a negotiated cost split between slow deploys and cut requests, not a technical constant.

open as a page

What does http.Server.RegisterOnShutdown do, and why must its function not block?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

RegisterOnShutdown records a callback that Server.Shutdown fires, each in its own goroutine, right after it closes the listeners. Shutdown never waits for those callbacks and ignores what they do, so they are a notification hook, not part of the drain.

open as a page