Ch. 20 · Networking

HTTP/3 and QUIC Transport

Why QUIC moves HTTP onto UDP, how browsers discover HTTP/3 through Alt-Svc and HTTPS records, and where its gains over HTTP/2 disappear.

~6 min readadvancedupdated Oct 19, 2026

HTTP/3 doesn’t change what an HTTP request means. Methods, status codes and headers are the same as in HTTP/2. What changes is everything underneath: instead of TCP plus TLS, HTTP/3 runs over QUIC, a transport protocol (RFC 9000) that lives on top of UDP. Most interview questions about HTTP/3 are really questions about why anyone would replace TCP, and what that costs.

The problem HTTP/2 couldn’t fix

HTTP/2 multiplexes many requests as streams over one TCP connection. That removed HTTP/1.1’s application-level head-of-line blocking, where a slow response held up the ones behind it on the same connection. But it moved the problem down a layer.

TCP delivers one ordered byte stream. It has no idea that bytes 40,000 to 41,400 belong to an image and bytes 41,400 to 42,800 belong to a JavaScript file. If the packet carrying the image’s bytes is lost, the kernel holds every later byte, including the complete JavaScript, until the retransmission arrives. One lost packet stalls all streams on the connection for at least a round trip.

On a fibre connection with almost no loss you rarely notice. On a phone with 2% packet loss and 150 ms round trips, it shows up as a page that loads in uneven bursts. HTTP/2’s single connection can even do worse than HTTP/1.1’s six parallel connections there, because with six connections a loss only stalls one of them.

QUIC’s fix is to make streams a transport concept. Each QUIC packet carries frames, and a STREAM frame says “these bytes belong to stream 8 at offset 12,000”. The receiver reassembles each stream separately. If a packet with stream 8’s data is lost, stream 12’s data in the next packet can be handed to the application immediately. Ordering is guaranteed within a stream, never across them.

A side effect: HTTP/2’s header compression, HPACK, assumes headers arrive in order, because each header block updates a shared table. HTTP/3 replaces it with QPACK, which moves table updates onto dedicated streams so that a lost packet doesn’t block decoding of unrelated requests.

What QUIC rebuilt on top of UDP

UDP gives QUIC almost nothing: ports and a checksum. Everything TCP provides had to be rebuilt in QUIC itself, usually in user space rather than the kernel:

  • Reliability and congestion control (RFC 9002). QUIC acknowledges packets with ACK frames and retransmits lost data, not lost packets. Packet numbers are never reused, so a retransmission gets a new number. That removes TCP’s “was this ACK for the original or the retransmit?” ambiguity and makes round-trip estimates more accurate.
  • Flow control, both per stream and per connection, so one greedy stream can’t fill the receiver’s buffers.
  • Encryption of almost everything. Only a few header fields stay visible. Packet numbers, ACKs and stream frames are encrypted. This was deliberate: middleboxes that inspect and “help” with TCP headers have made TCP very hard to change, and QUIC avoids giving them anything to depend on.
  • Connection IDs instead of the four-tuple. A TCP connection is identified by source and destination IP and port; if your phone moves from Wi-Fi to cellular, its address changes and every connection dies. QUIC connections are identified by IDs carried in the packets, so the connection can migrate to the new path after a quick path validation.

The handshake: fewer round trips, with one catch

A new HTTPS connection over TCP with TLS 1.3 costs one round trip for TCP’s SYN/SYN-ACK and another for the TLS handshake before the first request can go out. QUIC merges them: the client’s first packet carries the TLS 1.3 ClientHello inside a CRYPTO frame, the server answers with its handshake, and after one round trip the client can send its request. On a 150 ms mobile path that’s 150 ms saved on every new connection.

For a server the client has visited before, QUIC supports 0-RTT: the client uses a session ticket to encrypt its first request and sends it alongside the handshake. The catch is replay. An attacker who captures that first flight can send it again, and the server can’t fully tell the copies apart. So 0-RTT data should only carry requests that are safe to repeat, such as GETs without side effects. Servers that aren’t sure can reject it, and HTTP defines 425 Too Early for a server that wants the client to retry after the handshake completes. If an interviewer asks “is 0-RTT free?”, that’s the answer they’re after.

How a browser finds out HTTP/3 is available

A browser can’t simply try QUIC first everywhere; UDP 443 is blocked on plenty of corporate networks. So servers advertise it. The traditional way is the Alt-Svc response header on an ordinary HTTP/2 response:

$ curl -sI https://example.com | grep -iE "^(HTTP|alt-svc|server)"
HTTP/2 200
server: cloudflare
alt-svc: h3=":443"; ma=86400
Terminal

That says “HTTP/3 is available on UDP port 443 of this host; remember that for 86,400 seconds”. The first visit still goes over TCP, and later connections may use QUIC. Note the curl that produced this output is macOS’s bundled 8.7.1, which wasn’t built with HTTP/3 support (curl --http3 fails with the installed libcurl version doesn't support this), so it can read the advertisement but not act on it. curl -V lists HTTP3 under Features on builds that can.

The newer way is the HTTPS DNS record (RFC 9460), which lets a client learn about HTTP/3 before its first connection. The macOS dig is old enough to print it as raw TYPE65:

$ dig @1.1.1.1 cloudflare.com TYPE65 +noall +answer
cloudflare.com.		300	IN	TYPE65	\# 61 0001000001000602683302683200040008681084E5681085E5000600 20260647000000000000000000681084E52606470000000000000000 00681085E5
Terminal

Decoding those bytes with a short Python script gives priority 1, target . (the same name), alpn = h3,h2, ipv4hint = 104.16.132.229, 104.16.133.229 and two IPv6 hints. A browser that queries this record alongside A and AAAA can open a QUIC connection straight away.

Even then, clients race. Chrome typically attempts QUIC and keeps TCP as a fallback, and if QUIC fails on a network it remembers that for a while. Your server must keep serving HTTP/2 over TCP indefinitely.

Where HTTP/3 doesn’t help much

Expect modest gains, and sometimes none, in these cases:

  • Low-loss, low-latency paths. Between services in one data centre, head-of-line blocking is rare and the handshake saving is tiny. Service-to-service traffic usually stays on HTTP/2 or gRPC over TCP.
  • Networks that block or throttle UDP. Clients fall back to TCP, sometimes after a delay.
  • CPU-bound servers. TCP benefits from decades of kernel and NIC offload work. QUIC stacks run in user space and encrypt every packet header, so the same throughput can cost noticeably more CPU, although techniques like UDP generic segmentation offload narrow the gap.
  • Load balancers that route by four-tuple. After a client migrates, its packets arrive from a new address. A layer-4 load balancer has to route by QUIC connection ID, or migrated connections land on the wrong backend.

Practice question: the disappointing rollout

Your team enabled HTTP/3 on the CDN. A week later, dashboards show only 35% of requests on h3 and no change in median page load time. A manager asks whether to turn it off. How do you analyse it?

Answer, with reasoning

Neither number is surprising on its own. The 35% is expected because discovery is gradual: with Alt-Svc alone, a client’s first connection to each host uses TCP, so short sessions never upgrade. Some clients (older browsers, many HTTP libraries, bots) don’t support HTTP/3, and some networks block UDP. Publishing an HTTPS DNS record with alpn=h3 would let supporting clients use QUIC from the first connection.

The flat median is also expected. Users on good connections with little loss gain one round trip per new connection and not much else. QUIC’s benefits concentrate in the tail: lossy mobile networks, high-latency regions and users switching networks. I’d split the metrics by protocol and look at p90 and p99 time to first byte and load time, segmented by connection type and country, comparing h3 sessions with h2 sessions on similar networks. I’d also check server CPU per request, since QUIC can cost more. If the tail improved and CPU is acceptable, keep it on. HTTP/2 keeps running for everyone else anyway, so there’s little risk in leaving it enabled.

More in Networking

read ✓Networking · hard

Networking: The TLS Handshake

Read a real TLS 1.3 handshake from openssl s_client, compare it with TLS 1.2, and diagnose the certificate errors that break HTTPS clients.

~6 min readread →
read ✓Networking · easy

UDP Delivery and Application Responsibilities

What UDP gives up compared with TCP, shown with real Node experiments, and the sequencing, pacing and retry work your application takes on.

~6 min readread →
esc