Skip to content

HTTP/3, QUIC and UDP

HTTP/2's multiplexing solved head-of-line blocking at the HTTP layer, and then ran straight into a version of the same problem one layer down: TCP delivers bytes in one strict order, so a single lost segment stalls every HTTP/2 stream on the connection, not just the one the lost data belonged to. HTTP/3, standardized in 2022 as RFC 9114, doesn't try to work around that limit — it removes TCP from the picture entirely and replaces it with QUIC, a transport protocol built on top of UDP.

Why UDP, when TCP already does reliability well

This sounds backward at first: TCP already provides ordering, retransmission, and congestion control, and UDP provides none of that — so why would a protocol chasing reliability improvements move to the transport with fewer guarantees, not more?

The answer isn't that QUIC wants less reliability. It's that TCP bundles ordering and reliability into a single mechanism that applies to the entire connection as one undifferentiated stream, and that bundling is exactly what caused HTTP/2's remaining head-of-line blocking problem. QUIC rebuilds reliability and ordering itself, in user-space software rather than the operating system's kernel, but does it per stream instead of per connection — so loss affecting one stream's data can be recovered without holding up any other stream's data, which is precisely the property TCP structurally can't offer. Building on UDP is what makes this possible in practice: UDP itself does nothing but deliver individual datagrams, unordered, with no relationship enforced between one datagram and the next, leaving QUIC free to define its own ordering and recovery rules on top rather than inheriting TCP's connection-wide ones.

There's a second, more practical reason UDP was the pragmatic choice: TCP's core behavior is implemented deep in operating system kernels, and changing it — even in ways everyone agrees would help — moves at the pace of OS updates rolling out across the entire installed base of servers, routers, and middleboxes on the internet, many of which assume TCP behaves in specific, long-established ways. A transport built in user-space software over plain UDP can be updated, iterated on, and deployed at the pace of a browser or server release instead.

Per-stream loss recovery, and what it actually fixes

Picture the video call scenario from the previous article again, but running over HTTP/3 instead of HTTP/2: video and chat travel as two separate QUIC streams, multiplexed on one QUIC connection exactly the way HTTP/2 multiplexes streams on one TCP connection. The difference shows up the moment a packet carrying video data is lost. QUIC still needs to retransmit that lost data before the video stream can continue in order — video's own reliability guarantee doesn't disappear — but a chat message sent moments later, on its own separate stream, arrives and gets delivered to the application immediately, without waiting for the video stream's retransmission to complete. Loss is isolated to the stream it actually affects, instead of blocking the connection as a whole.

graph TB
    subgraph "HTTP/2 over TCP — one lost segment blocks everything behind it"
        A1[Stream 1 data] --> B1["Stream 2 data<br/>(LOST)"] --> C1[Stream 1 data — held up]
    end
    subgraph "HTTP/3 over QUIC — loss is isolated per stream"
        A2[Stream 1 data — delivered normally] 
        B2["Stream 2 data<br/>(LOST, being retransmitted)"]
        C2[Stream 1 data continues independently]
    end

This matters most exactly where HTTP/2's blocking hurt most: lossy networks. A stable, wired connection rarely loses packets, so the practical gap between HTTP/2 and HTTP/3 there is small. A mobile connection under load, or congested Wi-Fi, loses packets routinely — and that's where per-stream recovery turns into a real, felt difference rather than a theoretical one.

TLS is built in, not layered on top

HTTPS runs HTTP over a TLS-encrypted TCP connection — two separate handshakes stacked on top of each other, one to establish the TCP connection and a second, subsequent one to establish TLS on top of it. QUIC folds encryption into the transport itself: the QUIC handshake and the TLS 1.3 handshake happen together, as one combined exchange, rather than as two sequential round trips. A first-time connection to a QUIC server typically completes in one round trip instead of the two-plus a fresh HTTPS-over-TCP connection needs.

QUIC also supports 0-RTT for servers a client has connected to before: using key material cached from an earlier session, the client can send application data — an actual HTTP request — in its very first packet, before completing a new handshake at all. This is a meaningful latency win for repeat visits, but it comes with a genuine security trade-off the specification is explicit about: 0-RTT data has no protection against replay — a copy of that first packet, captured and resent by an attacker, can't be distinguished by the server from the original. This is exactly why 0-RTT is only considered safe for requests whose method is idempotent and side-effect-free, and why a well-behaved client won't send a POST — creating an order, submitting a payment — as 0-RTT data even when it's available.

Connection migration: surviving a change of IP address

TCP identifies a connection by a four-part tuple: source IP, source port, destination IP, destination port. Change any of those — a phone moving from Wi-Fi to a cellular network mid-download is the everyday case, since it gets a new IP address the moment it switches — and the old TCP connection is dead; the client has to open a brand new one, repeat the handshake, and (for HTTPS) repeat the TLS handshake too, from scratch.

QUIC identifies a connection with its own, independent connection ID, generated at the start of the connection and carried in every packet — deliberately decoupled from the client's IP address and port. When a client's network path changes, it can keep using the same connection ID on packets sent from its new IP address, and a QUIC-aware server recognizes the connection ID and continues the existing connection rather than treating the new packets as an unrelated new client. The download that would have stalled and restarted on a Wi-Fi-to-cellular handoff over HTTP/2 can, over HTTP/3, continue without the application ever seeing an interruption.

Fragmentation avoidance carries over from this course's earlier coverage

This isn't the first time this course has touched QUIC. The fragmentation article already noted that QUIC disables IP-level fragmentation for its own packets entirely, handling any necessary size adaptation itself, above IP — a direct, deliberate response to the same fragility that article covered in detail. It's worth re-reading that connection now that QUIC's full design context is in view.

Confirming HTTP/3 is actually in use

Because QUIC runs on UDP, a packet capture of HTTP/3 traffic looks structurally different from the TCP segments the earlier articles in this module showed:

sudo tcpdump -i eth0 -n udp port 443 -c 5
14:02:11.220104 IP 10.0.2.15.58211 > 203.0.113.40.443: UDP, length 1232
14:02:11.244890 IP 203.0.113.40.443 > 10.0.2.15.58211: UDP, length 1350
14:02:11.245310 IP 203.0.113.40.443 > 10.0.2.15.58211: UDP, length 1350
14:02:11.301220 IP 10.0.2.15.58211 > 203.0.113.40.443: UDP, length 210

There's no handshake flags, no sequence numbers visible at this level — QUIC's own framing and any handshake detail are encrypted inside the UDP payload, which is itself a deliberate design choice: encrypting even QUIC's own control information, not just the HTTP data carried inside it, denies middleboxes and passive observers the kind of protocol-internals visibility that has historically made evolving TCP's behavior across the internet so slow. A curl build with HTTP/3 support confirms the version actually negotiated, from the client's own perspective:

curl --http3 -v https://example.com/ 2>&1 | grep -i "using http\|HTTP/3"
* Using HTTP/3, server supports multiplexing
< HTTP/3 200

Where this module leaves off

Across HTTP, HTTPS, and these three evolution articles, this module has covered a single line of protocols — one request/response model, carried over three progressively less TCP-dependent transports. The rest of this module turns to a different, older family of application-layer protocols, built for specific jobs HTTP was never meant to do: moving files between hosts, and moving email between mail servers and the clients that read it.

Sources