CDN Edge Tuning for QUIC & HTTP/3
UDP-based QUIC transport eliminates the transport-layer head-of-line blocking that plagues HTTP/2 over TCP, but only when the CDN edge is configured to advertise, negotiate, and schedule HTTP/3 streams correctly. This page covers every layer of that configuration: ALPN negotiation, TLS 1.3 handshake optimization, RFC 9218 stream priority mapping, connection migration, and the telemetry workflows that prove QUIC is actually being used in production.
What QUIC Changes at the Transport Layer
HTTP/2 multiplexes streams over a single TCP connection. TCP’s ordered delivery guarantee means that when one segment is lost, the TCP layer must retransmit it before delivering any data from subsequent segments — stalling every HTTP/2 stream simultaneously. This is transport-layer head-of-line blocking, and it is structural to TCP; no HTTP/2 tuning can eliminate it.
QUIC replaces TCP with a UDP-based transport. Each QUIC stream has independent flow control: a lost UDP packet triggers retransmission only for the stream that owns that packet number. All other streams continue delivering data. This is the fundamental difference — not an incremental improvement but a protocol boundary.
Engine-level behaviour differences determine how this plays out in practice:
| Concern | Chromium (Chrome/Edge) | WebKit (Safari) | Gecko (Firefox) |
|---|---|---|---|
| HTTP/3 enabled by default | Yes (Chrome 87+) | Yes (Safari 14+) | Yes (Firefox 88+) |
| 0-RTT early data | Supported; gated by server quic_retry |
Supported | Supported |
| Connection migration on IP change | Supported (QUIC CID-based) | Partial (Wi-Fi→LTE tested) | Experimental flag |
Priority header (RFC 9218) |
Full support (Chrome 96+) | Partial | Partial |
| Alt-Svc cache persistence | Persistent across restarts | Session-scoped | Session-scoped |
| UDP/443 fallback heuristic | QUIC timeout → TCP after ~300 ms | Similar | Similar |
Chromium’s QUIC implementation is the most complete. When targeting a wide audience, prioritize validating behaviour in Chrome DevTools, then cross-check Safari’s session-scoped Alt-Svc behaviour.
Spec & API Reference: Key Directives and Headers
Alt-Svc: advertising HTTP/3 availability
Alt-Svc tells a browser that a superior protocol is available at a given host and port. The browser caches this and upgrades on the next connection. Without this header, HTTP/3 is never negotiated for new visitors.
| Field | Value | Notes |
|---|---|---|
h3=":443" |
QUIC on same port | Standard configuration for co-located HTTP/2+HTTP/3 |
ma=86400 |
Max-age in seconds | 24 h cache; set lower during rollout for faster fallback |
h3-29=":443" |
Draft-29 alias | Legacy; only needed for Chrome < 91 |
clear |
Invalidate cache | Forces browsers to forget a previously advertised Alt-Svc |
Priority header (RFC 9218)
RFC 9218 replaces HTTP/2’s binary dependency tree with a flat urgency field. Browsers emit Priority on HTTP/3 requests; edges can also respond with Priority to advise clients.
Urgency (u=) |
Semantic | Maps from fetchpriority |
|---|---|---|
| 0 | Highest — render-blocking | fetchpriority="high" on LCP image or critical CSS |
| 1 | High | Preloaded fonts, above-fold async scripts |
| 2 | Medium-high | Render-blocking scripts without explicit hint |
| 3 | Medium (browser default) | Most resources without an explicit hint |
| 4 | Medium-low | Below-fold images, deferred JS |
| 5–6 | Low | Prefetched pages, speculative loads |
| 7 | Lowest | Background analytics, non-critical beacons |
i (incremental) |
Can interleave | Safe for streaming or chunked responses |
QUIC transport directives (Nginx)
| Directive | Purpose |
|---|---|
quic_gso on |
Enable Generic Segmentation Offload for UDP batching — reduces CPU per packet |
quic_retry on |
Issue a Retry packet to validate client address before 0-RTT data |
quic_idle_timeout 30s |
Close idle QUIC connections; tune based on observed connection churn |
ssl_early_data on |
Enable 0-RTT; combine with quic_retry for replay protection |
http3_max_concurrent_pushes 0 |
Disable HTTP/3 push (deprecated in favour of 103 Early Hints) |
Step-by-Step Implementation
Step 1 — Enable the QUIC listener and advertise HTTP/3
# nginx.conf (nginx-quic build or Nginx 1.25+)
server {
# Bind UDP/443 for QUIC alongside the existing TCP listener
listen 443 quic reuseport; # UDP — required for QUIC
listen 443 ssl; # TCP — retained for HTTP/1.1 and HTTP/2
ssl_protocols TLSv1.3; # QUIC mandates TLS 1.3 — older versions are rejected
ssl_early_data on; # Allow 0-RTT on resumption (combine with quic_retry)
quic_gso on; # Batch UDP segments via GSO; cuts CPU ~20% on high-traffic edges
quic_retry on; # Validate client address before accepting 0-RTT early data
# Tell browsers HTTP/3 is available; 86400 s = 24 h Alt-Svc cache
add_header Alt-Svc 'h3=":443"; ma=86400' always;
add_header QUIC-Status $quic always; # Log actual negotiated protocol for debugging
}
Why reuseport: QUIC’s connection ID demultiplexing requires that all UDP datagrams for a connection reach the same worker process. reuseport lets the OS distribute connections across workers while the QUIC stack routes by connection ID within each worker.
Step 2 — Map fetch-priority signals to RFC 9218 Priority headers
The browser emits Priority: u=N on HTTP/3 requests based on its internal heuristics and any fetchpriority attribute. Confirm the edge is not stripping or overriding these headers:
# Preserve client-supplied Priority header downstream; do not rewrite it
proxy_pass_header Priority;
# For static assets served directly, inject a conservative default
# so the browser doesn't have to guess urgency
location ~* \.(css|js)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
# No Priority override here — browser's inferred value is correct for most assets
}
location ~* \.(webp|avif|jpg|png)$ {
# Below-fold images: signal low urgency so LCP image isn't starved
add_header Priority "u=4" always;
}
For LCP images, the urgency must be signalled from the HTML side via fetchpriority="high" — the browser translates this to Priority: u=0 on the HTTP/3 request automatically. You can verify this in the Network panel (see Verification Workflow below).
Step 3 — Configure connection-level QUIC timeouts
# Align idle timeout with your application's session keep-alive
quic_idle_timeout 30s; # Close idle QUIC connections; reduce if you see connection churn
# Initial congestion window: larger values help on high-BDP links (e.g. transatlantic CDN PoPs)
# Requires a QUIC build with BBR or CUBIC support
# quic_cc bbr; # Enable BBR congestion control where supported
Step 4 — Validate the Alt-Svc cache and upgrade path
The first request from a new visitor always uses TCP (HTTP/2 or HTTP/1.1) because no Alt-Svc entry is cached yet. Only from the second connection onward does the browser attempt QUIC. Test this sequence explicitly:
# Confirm Alt-Svc header is present on the initial TCP response
curl -sI --http2 https://example.com | grep -i alt-svc
# Expected: Alt-Svc: h3=":443"; ma=86400
# Confirm QUIC upgrade on subsequent connection (requires curl with QUIC support)
curl -sI --http3 https://example.com | grep -i quic
Step 5 — Wire priority hints in the application layer
<!-- LCP image: fetchpriority="high" maps to Priority: u=0 on the HTTP/3 request -->
<img src="/hero.avif" fetchpriority="high" loading="eager" alt="Hero image">
<!-- Below-fold images: explicit low priority prevents bandwidth contention during LCP -->
<img src="/gallery-1.avif" fetchpriority="low" loading="lazy" alt="Gallery image">
// Programmatic fetch: align priority with the resource's role in the render pipeline
// 'high' → browser emits Priority: u=0 on the QUIC stream
fetch('/api/critical-config', {
priority: 'high', // Scheduling rationale: needed before first render
cache: 'force-cache'
});
// Background analytics: low priority avoids starving render-critical streams
fetch('/analytics/beacon', {
priority: 'low', // Scheduling rationale: non-blocking, can wait
keepalive: true
});
Verification Workflow
Chrome DevTools: confirm HTTP/3 delivery
- Open DevTools → Network panel.
- Right-click any column header → enable Protocol.
- Reload the page.
- Filter by
h3orquicin the Protocol column. Resources showingh3are using QUIC transport. - Click a resource → Timing tab. A QUIC connection shows Connection Start near zero on resumption (0-RTT) or a single combined SSL + Connection entry for 1-RTT.
- Check that LCP image entries show
h3with aPriority: u=0request header. Open the resource → Headers → Request Headers.
PerformanceObserver: RUM protocol tracking
// Emit a RUM event for every resource, tagging the negotiated protocol
// and TTFB so you can track QUIC delivery in production
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (entry.entryType !== 'resource') continue;
const protocol = entry.nextHopProtocol; // 'h3', 'h2', or 'http/1.1'
const ttfb = entry.responseStart - entry.startTime;
// Report to your analytics pipeline
analytics.track('resource_timing', {
url: entry.name,
protocol,
ttfb: Math.round(ttfb),
duration: Math.round(entry.duration),
initiatorType: entry.initiatorType
});
}
});
observer.observe({ type: 'resource', buffered: true });
// Also check the navigation entry — confirms whether the HTML document itself used QUIC
const [nav] = performance.getEntriesByType('navigation');
console.log('Document protocol:', nav.nextHopProtocol);
Wireshark: packet-level QUIC inspection
For deep debugging of retransmission spikes or connection migration failures:
- Apply display filter:
udp.port == 443 && quic - Look for
QUIC STREAMframes. High retransmission counts on a specific stream indicate MTU or path interference. CRYPTOloss orACKstorm indicates firewall interference with QUIC handshake packets.- Connection migration appears as
PATH_CHALLENGE/PATH_RESPONSEframe pairs with a different source IP.
Lighthouse: automated audit
Lighthouse does not have a dedicated HTTP/3 audit, but the Uses efficient cache policy and Server response times audits are sensitive to protocol efficiency. Run with --throttling-method=devtools on a profile that has the Alt-Svc cache warm to get realistic QUIC TTFB measurements.
Edge Cases & Gotchas
0-RTT replay exposure
Enabling ssl_early_data allows the client to send HTTP requests in the first QUIC flight, before the server confirms the connection is fresh. Any idempotent GET request can be safely replayed; POST, PUT, and DELETE must not. The quic_retry directive forces the edge to issue a Retry packet and validates a token before accepting early data — this adds one round trip for new connections but eliminates replay risk. Configure your origin to reject 0-RTT on mutating endpoints:
# Reject early data on routes that modify state
location /api/ {
if ($ssl_early_data = "1") {
# Return 425 Too Early to signal the client must retry with 1-RTT
return 425;
}
proxy_pass http://backend;
}
UDP/443 firewall blocking and fallback
Corporate firewalls and some mobile carriers drop UDP/443 silently. Browsers detect this by measuring QUIC handshake timeout (typically 300 ms in Chromium) and downgrade to TCP. The Alt-Svc: clear response header lets you remotely invalidate cached Alt-Svc entries when you need to roll back HTTP/3 quickly across all browsers. Set ma to a lower value (e.g. 300) during initial rollout so fallback takes effect faster.
Cache key fragmentation
Over-segmenting edge cache keys by protocol (h2 vs h3) fragments stored content and reduces hit ratios. QUIC can serve the same byte ranges from entries already cached under HTTP/2. Use URL + Vary headers as the sole cache dimensions. If your CDN vendor creates protocol-specific cache namespaces, disable that feature.
HTTP/2 server push deprecation
HTTP/2 server push was intended to pre-deliver resources before the browser requested them. Chrome removed support in 2022. HTTP/3 inherits this removal. The correct replacement is 103 Early Hints, which lets the server emit preload Link headers before the full response is ready — without the cache poisoning risk that made push problematic. Ensure your edge CDN emits 103 rather than attempting PUSH_PROMISE frames.
Connection coalescing and QUIC
Connection coalescing allows a browser to reuse an existing HTTP/2 or HTTP/3 connection for a second origin if both origins resolve to the same IP and present the same TLS certificate (a wildcard or SAN cert). This means assets on static.example.com can be served over the same QUIC connection as www.example.com, saving an additional handshake. Verify this with the Connection ID column in DevTools Network panel — coalesced requests share the same ID.
MTU and QUIC packet sizing
QUIC packets over UDP are subject to path MTU. The default safe MTU is 1200 bytes (QUIC spec minimum). Paths with a smaller MTU (e.g. tunneled corporate networks) force QUIC to split data into smaller datagrams, increasing overhead. If you observe high retransmission rates in Wireshark, test with quic_mtu 1200 explicitly and check whether increasing it to 1400 bytes (common for Ethernet) improves throughput on your target geographies.
FAQ
Does enabling HTTP/3 on the CDN automatically make all users use QUIC?
No. The first connection from any new visitor always uses TCP because no Alt-Svc entry is cached yet. QUIC is attempted from the second connection onward. Users behind UDP-blocking firewalls will permanently fall back to HTTP/2. A realistic production QUIC adoption rate is 60–80 % of traffic for consumer audiences and lower for enterprise audiences.
How do I roll back HTTP/3 if I see problems in production?
Respond with Alt-Svc: clear from your origin. This instructs browsers to invalidate their cached Alt-Svc entries and stop attempting QUIC for your domain. Because the cache is session-scoped in Safari and persistent in Chrome, expect Chrome users to clear within the cached ma period. Set a low ma value (300 s) during rollout to minimize this window.
Should I enable HTTP/3 push (PUSH_PROMISE)?
No. HTTP/3 inherits HTTP/2’s push mechanism at the framing level, but Chrome and Firefox removed support. Use 103 Early Hints instead — your CDN edge can emit preload link headers for render-critical CSS and fonts as soon as it receives the request, before the origin responds.
What quic_idle_timeout value should I use?
Match it to your CDN’s TCP keep-alive timeout and your application’s session duration. A value of 30 s is conservative and safe. Setting it too low causes excessive connection churn on slow pages; too high keeps connections open past their useful life, wasting server memory. Monitor connection reuse ratios per PoP — if reuse is below 70 %, raise the timeout.
Does QUIC improve LCP on high-latency connections?
Yes, measurably. On connections with round-trip times above 100 ms, the 0-RTT resumption eliminates one full RTT from TTFB. Combined with the absence of transport-layer head-of-line blocking, LCP typically improves 5–15 % on mobile networks in high-latency regions, though the exact gain depends on the number of render-blocking resources and their sizes.
Related
- HTTP/2 & HTTP/3 Multiplexing & Connection Optimization — parent section
- Mitigating Head-of-Line Blocking — transport-layer HOL blocking mechanics
- HTTP/2 Stream Prioritization & Weighting — stream dependency trees and weight allocation
- Connection Coalescing & Domain Sharding — reusing QUIC connections across origins