TL;DR: HTTP/2 runs over TCP, so one lost packet stalls every request on the connection. HTTP/3 runs over QUIC on UDP, where a loss only stalls the streams it hit, and a connection can survive a switch from Wi-Fi to cellular. Serve HTTP/3 with HTTP/2 as the TCP fallback, and check what clients actually negotiate: an
Alt-Svcheader or a workingcurl --http3doesn’t mean HTTP/3 was used.
The Core Difference
Both protocols multiplex many requests over one connection, and both carry the same methods, status codes and headers. The difference is underneath. For HTTPS, HTTP/2 runs over TLS over TCP. HTTP/3 runs over QUIC on UDP, and QUIC has TLS built in. The HTTP/3 setup guide covers server configuration; this page is about what changes when packets get lost or the client’s network changes.
| Behavior | HTTP/2 over HTTPS | HTTP/3 |
|---|---|---|
| Transport | One ordered TCP byte stream | Independently ordered QUIC streams over UDP |
| Encryption | TLS 1.2 or higher | QUIC v1 uses TLS 1.3 or higher |
| Missing transport data | Can stall delivery for every HTTP stream on the connection | Missing bytes block their stream, not unrelated stream delivery |
| Header compression | HPACK | QPACK; decoding can wait for dynamic table entries |
| Client address change | TCP does not provide QUIC-style path migration | Connection IDs allow migration subject to protocol constraints |
| Negotiated ALPN | h2 | h3 |
| Network requirement | Reachable TCP listener | Reachable UDP listener; keep a TCP fallback |
The transport and TLS rules come from RFC 9113 sections 3 and 9.2 and RFC 9114 sections 1–3. The table assumes HTTPS; HTTP/2 also has a cleartext mode over plain TCP.
Packet Loss: Which Request Has To Wait
Say the browser requests /app.js and /account at the same time. In HTTP/2, frames from both streams go into one TCP byte stream. If a packet carrying part of /app.js is lost, TCP holds back everything after the gap, including /account bytes that arrived fine, until the retransmission lands. HTTP/2 multiplexing can’t fix this (RFC 9114 section 1.1).
QUIC tracks ordering per stream, so it can hand /account to the application while /app.js waits for its retransmission. One lost packet may carry data from several streams, and those streams all wait. Each stream is still delivered in order, and congestion control still applies to the whole connection (RFC 9000 sections 2 and 13).
So HTTP/3 has less blocking, not none. Header decoding can wait for QPACK dynamic table updates, and RFC 9204 section 2.1.2 defines these blocked streams explicitly. What HTTP/3 removes is TCP’s connection-wide delivery dependency.
Connection Migration: Wi-Fi To Cellular
A TCP connection is identified by its addresses and ports, so when your phone moves from Wi-Fi to cellular, the connection dies and the client reconnects. QUIC identifies connections with connection IDs instead. The client can move to the new network path, validate it, and keep the same connection. The same mechanism handles NAT rebinding, where a middlebox changes the client’s port mid-connection.
Not every handoff survives. A server can turn off active migration with the disable_active_migration transport parameter, migration isn’t allowed until the handshake is confirmed, and the new path has to actually work. See RFC 9000 section 9.
0-RTT: A Resumption Feature With Replay Risk
When a client resumes an earlier connection, QUIC lets it send application data in its very first flight (RFC 9001 section 4.6). This isn’t unique to HTTP/3: TLS 1.3 offers the same early data over TCP. The catch is the same in both cases. An attacker who captures early data can replay it (RFC 8446 section 2.3).
A sensible policy: accept replay-tolerant reads such as GET /logo.svg in early data, and answer an early POST /transfer with 425 Too Early. The client then retries without early data. If a gateway forwards a request before its own client handshake has finished, it adds Early-Data: 1, and the origin answers 425 if it can’t process that request safely. Waiting at the origin doesn’t help; the early data already crossed the earlier hop. These rules come from RFC 8470 sections 5–6.
Discovery And UDP Fallback
Servers usually advertise HTTP/3 with a response header. This one (an example) points to the same hostname on UDP port 443:
Alt-Svc: h3=":443"; ma=86400
ma=86400 is how long, in seconds, the client may remember the advertisement; it isn’t an HTTP/3 timeout. A client typically sees this on a TCP response and tries QUIC on a later connection (RFC 7838 section 3). To get in earlier, publish an HTTPS DNS record; clients that look it up learn about h3 before their first HTTP response (RFC 9460 section 10):
example.com. 300 IN HTTPS 1 . alpn="h3,h2"
Both are hints; neither forces HTTP/3. When UDP is blocked, RFC 9114 section 3.1 says clients should fall back to HTTP over TCP. So keep your TCP listener up. How quickly a client gives up on QUIC is up to the client.
When Each Fits
Keep HTTP/2: always, as the TCP path. It’s all you need when your clients or their networks can’t use QUIC, or when your endpoint only has a TCP listener today. Adding Alt-Svc won’t open a UDP port your firewall blocks.
Add HTTP/3 alongside it: when your traffic has many concurrent downloads over lossy links, or clients that switch networks mid-session, such as mobile apps. That’s what QUIC was designed for, but the gain varies by network. Measure real request timings on your own paths before declaring a winner.
The Common Mistake: Testing The Advertisement
Seeing h3 in Alt-Svc tells you the server sent the header. Whether UDP actually gets through is a separate question. Likewise, curl --http3 falls back to TCP when QUIC fails, so a successful response may have come over HTTP/2 or HTTP/1.1. curl documents both modes.
Check On The Wire
You need a curl build with HTTP/3 support; look for HTTP3 in the features line of curl -V. The %{http_version} write-out variable prints the version the request actually used (curl manual):
curl -V | grep -E 'Features:.*HTTP3'
curl -sS -D - -o /dev/null https://example.com/ | grep -i '^alt-svc:'
dig +short HTTPS example.com
curl --http2 -sS -o /dev/null -w '%{http_version}\n' https://example.com/
curl --http3 -sS -o /dev/null -w '%{http_version}\n' https://example.com/
curl --http3-only -sS -o /dev/null -w '%{http_version}\n' https://example.com/
Swap in your own endpoint. --http3-only turns off the TCP fallback, so it’s the real test. If it fails while --http3 succeeds, look at the version --http3 printed and check UDP reachability; QUIC isn’t working yet. --http2 negotiates too, so read its printed version as well.
In Chrome DevTools, turn on the Network panel’s Protocol column. It shows h2 or h3 for each request (Chrome documentation). Look at individual requests: one page load often mixes protocols, especially the first visit before the browser has seen Alt-Svc.
Related
- HTTP/3 and QUIC setup for listeners and firewall configuration.
- HTTP/1.1 vs HTTP/2 for multiplexing over TCP.
- Alt-Svc for alternative endpoint advertisements.
- 425 Too Early for handling replay-sensitive early requests.