What HTTP/3 Changes
HTTP/3 replaces TCP with QUIC (a UDP-based transport) for all HTTP communication. The practical differences that matter for website performance:
| Feature | HTTP/2 (TCP) | HTTP/3 (QUIC) |
|---|---|---|
| Connection setup | 2–3 round trips (TCP + TLS) | 1 round trip (0-RTT on reconnect) |
| Head-of-line blocking | One lost packet stalls all streams | Only the affected stream stalls |
| Connection migration | Breaks on network change (WiFi→cellular) | Survives network changes via connection IDs |
| Packet loss handling | TCP retransmission stalls everything | Per-stream retransmission, others continue |
When HTTP/3 helps most: High-latency connections (mobile, satellite, international users), lossy networks (WiFi, cellular), and users who switch networks mid-session. On fast, reliable connections (fiber, local CDN), the difference is small.
When HTTP/3 doesn't help: Same-origin requests on reliable networks, pages with few resources, or when the bottleneck is server processing time rather than transport latency.
How HTTP/3 Negotiation Works
HTTP/3 isn't used on the first visit to a site. The browser discovers HTTP/3 support through the Alt-Svc response header:
1. Browser connects via HTTP/2 (TCP) on first visit
2. Server responds with: Alt-Svc: h3=":443"; ma=86400
3. Browser remembers this for 86400 seconds (24 hours)
4. On next request, browser attempts QUIC (UDP port 443)
5. If QUIC fails, browser falls back to HTTP/2 (TCP)
This means: the first page load is always HTTP/2. HTTP/3 only kicks in on subsequent visits — and only if QUIC isn't blocked.
Diagnosing HTTP/3 Issues
Check If Your Server Advertises HTTP/3
# Look for the Alt-Svc header
curl -sI https://example.com | grep -i alt-svc
# Expected output for HTTP/3 support:
# alt-svc: h3=":443"; ma=86400
# If empty, your server isn't advertising HTTP/3
Check If HTTP/3 Actually Works
# Attempt an HTTP/3 connection (requires curl 7.88+ with HTTP/3 support)
curl --http3-only -I https://example.com
# If this fails, HTTP/3 isn't working even though it might be advertised
# Common error: "connection refused" = UDP port 443 blocked
Browser Verification
Chrome: Navigate to your site → DevTools → Network tab → right-click column headers → enable "Protocol" column. Look for h3 (HTTP/3) vs h2 (HTTP/2).
Chrome internal diagnostics:
chrome://net-internals/#alt-svc— Shows cached Alt-Svc entries (which sites the browser knows support HTTP/3)chrome://net-internals/#quic— Active QUIC sessions and connection detailschrome://flags/#enable-quic— Verify QUIC is enabled (it's on by default)
Firefox: about:networking#http3 — Shows active HTTP/3 connections
Network-Level Check
# Verify UDP port 443 is open (QUIC uses UDP, not TCP)
nc -zuv example.com 443
# Wireshark filter for QUIC traffic
# Filter: quic || udp.port == 443
Common Issues and Fixes
1. Server Not Advertising Alt-Svc
Symptom: Browser always uses HTTP/2 even though you think HTTP/3 is enabled.
Cause: Missing or incorrect Alt-Svc header. The server must explicitly tell browsers that HTTP/3 is available.
nginx (1.25.0+):
server {
listen 443 ssl;
listen 443 quic; # Enable QUIC listener
http2 on;
http3 on; # Enable HTTP/3
# Tell browsers HTTP/3 is available
add_header Alt-Svc 'h3=":443"; ma=86400' always;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Required for QUIC
ssl_early_data on;
}
Caddy (automatic):
Caddy enables HTTP/3 by default with no configuration needed. It automatically sets the Alt-Svc header. Verify with curl -sI https://example.com | grep alt-svc.
Apache (mod_http2 doesn't support HTTP/3): Use a reverse proxy (Caddy, nginx) in front of Apache, or use a CDN that adds HTTP/3 at the edge.
2. UDP Blocked by Firewall or ISP
Symptom: Alt-Svc header is present but the browser never uses HTTP/3. Protocol column shows h2 on all subsequent visits.
Cause: Corporate firewalls, ISPs, or security appliances block UDP port 443. QUIC uses UDP instead of TCP, and many networks only allow TCP on port 443.
Diagnosis: Check Chrome's chrome://net-internals/#quic — if no QUIC sessions appear for your domain, UDP is likely blocked.
Fix:
- Server side: Ensure your firewall allows UDP port 443 inbound. For cloud providers:
- You can't fix client-side blocking. This is why HTTP/2 fallback must always work. Corporate networks often block UDP 443 — your site must serve content over HTTP/2 (TCP) when QUIC fails.
- CDNs handle this automatically: Cloudflare, Fastly, AWS CloudFront, and Google Cloud CDN all serve HTTP/3 at the edge with automatic TCP fallback.
3. CDN Not Passing Alt-Svc Header
Symptom: Origin server sends Alt-Svc but it doesn't appear in responses served through the CDN.
Cause: Some CDNs strip the Alt-Svc header from origin responses and add their own (or don't).
Fix — Enable HTTP/3 at the CDN level:
- Cloudflare: Dashboard → Speed → Optimization → Protocol Optimization → HTTP/3 (toggle on). Cloudflare adds its own Alt-Svc header at the edge.
- AWS CloudFront: Console → Distribution → Behaviors → Supported HTTP versions → check HTTP/3.
- Fastly: Dashboard → Origins → HTTP/3. Fastly handles Alt-Svc automatically when enabled.
4. Fallback Not Working (Broken When QUIC Fails)
Symptom: Users on networks that block QUIC experience page load failures or extremely slow loads.
Cause: Client attempts QUIC, waits for timeout (several seconds), then falls back to HTTP/2. In rare cases, broken QUIC implementations cause the fallback to also fail.
Fix: Modern browsers handle fallback automatically — they race QUIC and TCP connections and use whichever succeeds first (Happy Eyeballs v2 for QUIC). If you're seeing fallback issues:
- Ensure your server responds correctly on both TCP 443 (HTTP/2) and UDP 443 (QUIC).
- Set a reasonable
ma(max-age) in Alt-Svc:h3=":443"; ma=86400(24 hours is standard; use shorter values like 3600 during rollout). - Test by blocking UDP 443 locally and verifying the site loads via HTTP/2.
Measuring HTTP/3 Impact
Protocol Distribution
// What protocol are your real users getting?
const nav = performance.getEntriesByType('navigation')[0];
console.log('Page protocol:', nav.nextHopProtocol); // 'h3', 'h2', 'http/1.1'
// Protocol distribution across all resources
const protocols = {};
performance.getEntriesByType('resource').forEach(r => {
const proto = r.nextHopProtocol || 'unknown';
protocols[proto] = (protocols[proto] || 0) + 1;
});
console.log('Protocol distribution:', protocols);
Connection Time Comparison
// Compare connection setup time (where HTTP/3's 0-RTT shines)
const entries = performance.getEntriesByType('resource');
const byProtocol = { h3: [], h2: [] };
entries.forEach(r => {
const proto = r.nextHopProtocol;
const connectTime = r.connectEnd - r.connectStart;
if (proto && connectTime > 0) {
(byProtocol[proto] || []).push(connectTime);
}
});
Object.entries(byProtocol).forEach(([proto, times]) => {
if (times.length > 0) {
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`${proto}: avg ${Math.round(avg)}ms connect (${times.length} resources)`);
}
});
Server Configuration Reference
nginx (1.25.0+)
http {
server {
listen 443 ssl;
listen 443 quic reuseport; # reuseport on first server block only
http2 on;
http3 on;
http3_hq on; # Optional: HTTP/3 over QUIC (experimental)
quic_retry on; # Requires address validation token (mitigates amplification)
ssl_early_data on;
ssl_protocols TLSv1.3; # QUIC requires TLS 1.3
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
}
Caddy
example.com {
# HTTP/3 is enabled by default in Caddy 2.6+
# To explicitly control it:
servers {
protocols h1 h2 h3
}
}
LiteSpeed
LiteSpeed Web Server has built-in QUIC/HTTP/3 support. Enable in WebAdmin Console → Listeners → SSL → Enable QUIC.
Related Guides
- DNS Resolution Issues — DNS latency compounds with connection setup time
- Prefetch and Preconnect — preconnect establishes early connections (works with both HTTP/2 and HTTP/3)