skip to content

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

level: middleimportance: nice to knowfreq 24%

answer

  1. a list of niladic callbacks
  2. started, not joined
  3. fires with the listeners, not at the end
  4. signal, do not wait
  5. one of the two stop methods skips it

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.

solid answer

~50 s

`srv.RegisterOnShutdown(f)` appends `f` to a list the server keeps. When `Shutdown` runs, it closes the listeners and then starts every registered function as `go f()` before it begins waiting for connections. It never joins those goroutines, never sees a return value, and the context deadline does not apply to them. That makes it the right place to *signal* long-lived work the server does not track itself — telling a background loop to wind down, nudging connections you manage outside the request path — and the wrong place for cleanup that must finish, such as flushing a buffer or committing state, because the process may exit while the callback is still running. If something must complete, coordinate it yourself with a `sync.WaitGroup` or a channel joined after `Shutdown` returns. Note also that `Close` does not run these callbacks at all — only `Shutdown` does.

code

go · 13 lines
go
draining := make(chan struct{})
srv.RegisterOnShutdown(func() { close(draining) })

var bg sync.WaitGroup
bg.Add(1)
go func() {
	defer bg.Done()
	<-draining // told at the moment the listeners close
	windDown()
}()

_ = srv.Shutdown(ctx)
bg.Wait() // the waiting belongs here, not in the callback

go deeper

for a junior

Know that the method exists, takes a plain func(), and is fired by Shutdown rather than by Close. Do not expect it to do anything on its own.

for a middle

Explain that each callback runs in its own unwaited goroutine at the start of the drain, and why that makes it a signalling hook rather than a place for cleanup.

for a senior

Show a shutdown sequence that signals through the hook and does the actual waiting and dependency teardown after Shutdown returns, in a defined order.

for a principal

Decide whether this hook belongs in the service template at all, given how easily it is mistaken for a lifecycle guarantee, and what the alternative shared stop-sequence looks like.

## What the method is ```go func (srv *Server) RegisterOnShutdown(f func()) ``` It takes a niladic function and stores it on the server. You may call it any number of times before shutdown; each registered function is kept. There is no deregistration and no ordering guarantee you should rely on. ## When and how the callbacks run `Shutdown` runs them near the very start of its work: after it has closed the listeners and before it begins polling for connections to go idle. Each one is launched with `go f()` — its own goroutine, started and forgotten. `Shutdown` does not wait for any of them, has no way to observe whether they finished, and does not care if one panics or blocks forever. The context you passed to `Shutdown` is not passed to the callbacks and does not bound them. Two consequences follow directly: 1. **A blocking callback does not delay the drain**, which sounds convenient until you realise it also means the drain does not protect the callback. `Shutdown` can return and `main` can exit while your callback is halfway through its work. 2. **A callback cannot report failure.** There is no error channel and no return value. If you need to know whether the work succeeded, the callback has to publish that itself. ## What it is for The hook exists so that things the `http.Server` does not track can be *told* that shutdown has started. The server's drain is expressed entirely in terms of the connections it is serving requests on; anything long-lived that lives outside that accounting gets no notification from the drain itself. `RegisterOnShutdown` is the notification. A typical use is to close a channel that a background loop selects on: ```go draining := make(chan struct{}) srv.RegisterOnShutdown(func() { close(draining) }) ``` Every goroutine that cares can now select on `draining` and start winding down at the same moment the listeners close. What you must not do is put the *waiting* in the callback — put it after `Shutdown` returns, where you control it. ## The anti-patterns **Flushing in the callback.** `srv.RegisterOnShutdown(func() { telemetry.Flush(); db.Close() })` looks tidy and is a race: the flush is running concurrently with the drain and with your process exit, and closing the database out from under handlers that are still serving in-flight requests will make those requests fail — the exact requests the drain was protecting. Do teardown of shared dependencies *after* `Shutdown` returns, in a deliberate order. **Assuming Close runs them.** `Server.Close` closes listeners and connections and does not touch the registered functions. If a code path can end in `Close` — including the escalation after a `Shutdown` deadline — anything you needed from those callbacks has to be triggered elsewhere. **Using it as the only shutdown signal for handlers.** In-flight handlers are not notified by it either; the callbacks are separate goroutines, and a running handler learns nothing unless it happens to be watching a channel one of them closed. ## How to answer it in an interview Say what it is (a callback list), when it fires (start of `Shutdown`, after listeners close), how it fires (one goroutine each, unwaited, unbounded by the context), what that implies (signal, don't wait), and the `Close` asymmetry. That is the whole method, and knowing that `Shutdown` does not join those goroutines is the part that separates someone who has read the code from someone who assumed the name meant "run this before we stop".

  • Do the registered functions run if you call Server.Close instead of Server.Shutdown?
    No. `Close` closes the listeners and every tracked connection and returns; it never touches the registered callbacks. That matters for the common escalation path, where a `Shutdown` whose deadline expired is followed by `Close` — anything you depend on must already have been signalled by then.
  • Where should a database handle actually be closed during shutdown?
    After `Shutdown` returns, in the shutdown path, once no handler can still be using it. Closing it from a registered callback runs it concurrently with the drain and breaks the in-flight requests the drain exists to protect.

saying these in an interview costs you the question

  • Thinks Shutdown waits for the registered callbacks
  • Puts a flush or a Close of shared state in the callback
  • Assumes Server.Close also runs the callbacks
  • Expects the Shutdown context to bound the callbacks
  • Believes in-flight handlers are notified by the hook