Decision records
ADR-0041: HTTP/3 graceful lifecycle (single and multiple instances)
Status: accepted (design, D7) — 2026-10-07
Context
The HTTP/3 accept loop closed the whole QUIC endpoint (Endpoint::close) as soon as the
shutdown signal arrived. That sends CONNECTION_CLOSE on every connection at once: the
per-connection GOAWAY and the wait for in-flight requests never got a chance, so every
restart and every drain cut H3 requests — also with a single instance. Multi-instance
operation (ADR-0039) adds scale-out/in, demux registration and Alt-Svc timing.
Decision
Drain of an instance (SIGTERM, scale-in, rolling reload):
- stop placement of new connections on it: the instance tells the demux
draining(multi-instance) or stops accepting incoming connections on its endpoint (single); - keep receiving for its existing connections (the demux keeps routing its server id);
- send HTTP/3 GOAWAY on every connection (no new requests; in-flight ones continue);
- finish accepted requests;
- when a connection has no request left, close it with
H3_NO_ERROR; - after
h3_drain_timeout(default: the server’sshutdown_timeout) close what is left withH3_NO_ERRORand count it as forced; - wait until the endpoint is idle (close frames delivered, bounded) and exit.
An established connection is never redirected to another instance.
Scale-out: start → endpoint with the controller’s sockets and keys → register with the demux (not yet placeable) → readiness → activation makes it placeable. Existing connections stay where they are.
Alt-Svc: an instance advertises h3 only while its QUIC endpoint can take new
connections: single instance — endpoint open; multi-instance — registered with the demux
and not draining. When HTTP/3 is removed from the configuration, advertising stops first,
existing QUIC connections drain, the UDP socket closes last (ADR-0037).
Crash of an instance: its server id disappears from the demux; datagrams for it get a stateless reset from another instance; clients reconnect (new placement).
Crash of the demux: the controller restarts it with the same UDP sockets; instances reconnect and register again (with their draining state); datagrams queued in the kernel socket meanwhile are delivered afterwards, the rest are retransmitted by QUIC.
Controller restart: adopts demux and instances (pid + start time + executable, like instances, ADR-0035); asks the demux for its live registrations to rebuild the placement view.
Metrics
Per instance: active QUIC connections, H3 streams in flight, GOAWAYs sent, drain duration, graceful and forced closes, handshakes; per demux: datagrams in/out per instance, placements, unroutable datagrams, resets requested, drops (full instance socket).