Hooks are registered in phases, as in the README quick start: the HTTP server, NATS, the database, Redis and OpenTelemetry. Listen blocks until SIGTERM or SIGINT.
1The service is running
2SIGTERM arrives
3Stop accepting new requests
4Drain in-flight work
5Close clients, in parallel
6Flush logs and traces last
7Done: Listen returns
8If a hook hangs: the watchdog
9A second SIGTERM forces exit
←→ step · space play
All steps as text
The service is running. Hooks are registered in phases, as in the README quick start: the HTTP server, NATS, the database, Redis and OpenTelemetry. Listen blocks until SIGTERM or SIGINT.
SIGTERM arrives. The budget clock starts (WithBudget(30s) in the quick start). Phases run lowest value first. PhasePreShutdown (-100) has no hooks in this example, so it has nothing to do.
Stop accepting new requests. PhaseStopAccepting (0): the HTTP server stops taking new connections. The HTTP contrib registers here so the database and other clients stay open for the requests still in flight.
Drain in-flight work. PhaseDrainTraffic (100): NATS drains. A phase starts only after the one before it has finished.
Close clients, in parallel. PhaseFlushQueues (200) is empty here. In PhaseCloseClients (300) the db and redis hooks run at the same time, because hooks inside one phase run in parallel.
Flush logs and traces last. PhaseFlushLogs (400): OpenTelemetry flushes last, so the spans from the earlier phases actually leave the process.
Done: Listen returns. Every phase has run. Listen returns errors.Join of any hook errors and you decide how to exit, or let WithExitOnComplete(0, 1) exit for you.
If a hook hangs: the watchdog. Another run: redis never returns. After the budget plus the grace period (1 second by default), the watchdog calls os.Exit with the failure code (1 with WithExitOnComplete(0, 1)) and logs the stuck hook's name.
A second SIGTERM forces exit. If the operator sends SIGTERM again while shutdown is still running, the manager calls os.Exit(130) at once. This is the escape hatch when something hangs.
The quick start from the README, which the diagram follows:
Phases are plain int values, so you can pass any number to WithPhase to order a hook between the predefined phases. Set the budget a few seconds shorter than Kubernetes’ terminationGracePeriodSeconds so the service exits cleanly before it is killed.