Rolling Out Alt-Svc Headers Safely
The QUIC listener at the edge is live, curl --http3 works, yet the protocol split in your analytics barely moves off HTTP/2 — or worse, the day after you advertised h3 a subset of users on UDP-hostile networks started seeing multi-second stalls before every page load.
Root Cause: Discovery Is Cached State, and Cached State Can Strand You
HTTP/3 has a bootstrapping problem that HTTP/2 never had. ALPN negotiation inside a TLS-over-TCP handshake can upgrade a connection to h2 on the spot, but it cannot move a client to a different transport protocol — QUIC runs over UDP, so the browser has to be told that an alternative endpoint exists before it can try. The Alt-Svc response header is that advertisement: Alt-Svc: h3=":443"; ma=86400 says “an endpoint speaking the h3 ALPN token is reachable at this host on UDP port 443; you may treat it as authoritative for this origin for 86 400 seconds.” The browser stores this in a per-origin alternative-services cache and consults it on the next connection. Two consequences follow directly. First, a new visitor’s entire first session may run on TCP — the advert only pays off on a later connection, which is why sites with high first-visit traffic see structurally low h3 share. Second, and more dangerous: the advert is a cached promise, and if the promise is broken — a PoP whose UDP 443 is mistakenly filtered, an MTU black hole on the QUIC path, a middlebox that passes the handshake but drops short packets — clients keep believing it for up to ma seconds.
Browsers hedge against exactly this, which is both your safety net and the reason bad rollouts are hard to detect. When a cached h3 entry exists, Chromium races the QUIC connection against a TCP connection rather than betting everything on UDP; if QUIC’s handshake does not complete within roughly 300 ms of the TCP attempt succeeding, the request rides TCP and the browser marks QUIC as broken for that origin, backing off for the session (with exponentially growing memory in some implementations). Firefox and Safari implement similar happy-eyeballs-style races with session-scoped Alt-Svc caches. The failure mode this produces in production is not an outage — it is a tax: every affected user pays a UDP timeout or a lost race before every fresh connection, TTFB p95 climbs on exactly the networks least able to afford it, and your average protocol metrics look fine because the requests all eventually succeed over HTTP/2. The persist=1 parameter deepens the trap: it asks clients to keep the entry across network changes, so a user who cached the advert on home Wi-Fi carries it onto a corporate VPN where UDP 443 is dead.
The syntax itself has sharp edges too. Multiple alternatives are comma-separated and order expresses preference (h3=":443"; ma=86400, h3-29=":443"; ma=86400 for legacy draft support); the value clear invalidates every cached alternative for the origin; and an advert naming a different host or port only takes effect if the TLS certificate on the alternative validates for the original origin — a mismatch is silently ignored, which is a common reason adverts appear to “not work” behind multi-CDN setups. Finally, Alt-Svc has a designated successor: the HTTPS DNS resource record (SVCB family) carries alpn="h3" hints in the DNS answer itself, letting supporting browsers attempt QUIC on the very first connection. It removes the first-visit penalty but inherits none of Alt-Svc’s per-origin cache safety valves, so during rollout you should treat it as an accelerant to add after the Alt-Svc path is proven, not a replacement for it.
Protocol Discovery State Machine
Minimal Reproduction
The classic unsafe rollout: a long-lived, persistent advert shipped to 100% of traffic on day one, with no way to observe who fails.
# nginx 1.25+ — BROKEN rollout: maximum blast radius, zero observability
server {
listen 443 quic reuseport;
listen 443 ssl;
# 7-day cache + persist across network changes, advertised to everyone
# immediately. Any QUIC path defect is now pinned into client caches
# for up to 604800 s — the rollback window equals the advert lifetime.
add_header Alt-Svc 'h3=":443"; ma=604800; persist=1' always;
# No protocol logging: h2-vs-h3 share and fallback rate are invisible,
# so a stranded-user incident surfaces as vague "site feels slow" reports.
access_log /var/log/nginx/access.log combined;
}
Reproduce the stranding symptom on a test machine by advertising h3, letting the browser cache it, then blocking UDP 443 — every subsequent fresh connection now pays the race/timeout before settling on TCP:
# Simulate a UDP-hostile network after the advert is cached
sudo iptables -A OUTPUT -p udp --dport 443 -j DROP
# Reload the page with a cold socket pool: DevTools shows ~300 ms of extra
# connection setup while the QUIC attempt dies and TCP wins the race
Deterministic Fix Protocol
- [ ] 1. Instrument before you advertise. Add
$server_protocol/$quic(or the CDN’s protocol field) to access logs and deploy a RUM tag onPerformanceResourceTiming.nextHopProtocol. You need three numbers per geography: h3 share, QUIC-attempt failure rate, and TTFB split by protocol. Without these, a partial UDP outage is indistinguishable from normal variance. - [ ] 2. Verify the QUIC path end-to-end from outside your network. From several vantage points:
curl --http3-only -sv https://example.com/ -o /dev/null. Test both small responses and responses larger than 1200 bytes to catch MTU black holes that pass the handshake but kill data packets. - [ ] 3. Ship the advert with a short lifetime first.
add_header Alt-Svc 'h3=":443"; ma=300' always;— a 5-minute cache means a rollback propagates in minutes. Do not setpersist=1at this stage; you want entries dropped on network change while confidence is low. - [ ] 4. Canary by response subset, not by config flag. Emit the header only on a low-stakes path (for example
location /static/) or from a fraction of edge nodes, and compare protocol-split TTFB between canary and control. The browser caches the advert per-origin regardless of which response carried it, so even a static-asset canary upgrades the whole origin — size the canary population accordingly. - [ ] 5. Watch the fallback tax, not just the success rate. Plot p95 connection-setup time for users whose sessions show h2 after previously showing h3 — that cohort is paying the race penalty on hostile networks. If it exceeds a few percent of traffic in a geography, keep
mashort there or exclude those PoPs. - [ ] 6. Ratchet
maup in stages. 300 → 3600 → 86400 over one to two weeks of clean metrics. Each step multiplies both the h3 share (fewer expiry resets) and the rollback window; never advertise anmalonger than the time you are willing to live with a bad cached entry. - [ ] 7. Pre-stage the rollback. Keep a one-line change ready that swaps the header to
Alt-Svc: clearand, for true emergencies, an edge ACL that drops UDP 443 — handshake failure triggers immediate client-side TCP fallback, which propagates faster than cache invalidation ever can. - [ ] 8. Add the HTTPS DNS record last. Once Alt-Svc metrics are clean at
ma=86400, publish an HTTPS resource record withalpn="h2,h3"to remove the first-visit TCP penalty for supporting clients — the point at which returning visitors can also start banking 0-RTT resumption savings on top. Keep the Alt-Svc header in place — the record and the header serve different client populations, and the header remains your only lever forclear.
Before/After Metrics
E-commerce origin, global anycast CDN, four-week staged rollout (steps 1–8) versus the day-one ma=604800 big-bang attempted previously.
| Metric | Big-bang rollout | Staged rollout | Change |
|---|---|---|---|
| HTTP/3 traffic share (returning visitors) | 31% | 74% | +43 pts |
| QUIC attempt failure rate | 9.1% (undetected 3 days) | 1.8% (caught in canary) | −80% |
| p95 connection setup on UDP-blocked nets | +780 ms (persist pinned) | +45 ms (short ma, no persist) | −94% |
| Rollback propagation time | up to 7 days | under 5 minutes | −99% |
| TTFB p50 delta, h3 vs h2 cohort | not measurable (no split) | −61 ms | now visible |
| Support tickets citing slowness | 40/week spike | 0 attributable | eliminated |
The headline is not the h3 share — it is the rollback window. The big-bang advert converted a routine PoP misconfiguration into a week-long degradation for a pinned cohort; the staged advert made the same class of defect a five-minute non-event.
FAQ
Why is my HTTP/3 traffic share stuck around 20 percent even though everything is configured?
Three drains stack: first visits always start on TCP because no advert is cached yet; UDP-blocked networks (typically 5–15% of connections, higher for enterprise audiences) permanently fall back to HTTP/2; and a short ma left over from rollout lets entries expire between visits, resetting returning users to TCP discovery. Raise ma once burn-in is done and measure share on returning visitors only — that is the number the advert can actually influence. Also confirm your certificate covers the advertised endpoint; a SAN mismatch makes browsers ignore the advert silently, the same validation rule that governs connection coalescing.
If QUIC breaks in production, how fast does Alt-Svc: clear actually take effect?
Only as fast as clients reconnect over TCP and receive the clear. Browsers holding a live QUIC connection keep using it, and cached entries persist up to their remaining ma for clients who do not revisit — your worst-case exposure equals the largest ma you ever advertised. Dropping UDP 443 at the edge is the faster kill switch: the failed handshake triggers the client’s built-in TCP fallback immediately, no cache invalidation required.
Do I still need Alt-Svc once I publish an HTTPS DNS record with h3 in the ALPN list?
Keep both through the transition. The HTTPS record removes the first-connection TCP penalty for clients whose resolvers and browsers support it, but support is uneven and some networks strip the record type. Alt-Svc remains the discovery path for everyone else and the only mechanism with a clear escape hatch. Browsers merge the two signals; the record accelerates, the header governs.
Related
- CDN Edge Tuning for QUIC & HTTP/3 — parent section: the full edge configuration this rollout activates
- QUIC 0-RTT Session Resumption Gotchas — the next latency lever once discovery reliably lands users on h3