Skip to content
CodeBrewerz logoCodeBrewerz
Networking

HTTP/3: What's New, and Where It Actually Earns Its Keep

HTTP/3 doesn't run on TCP. That one sentence explains most of what changed — and most of why it helps some sites a lot and others barely at all.

Published 9 min readBy Piyush Jain
  • HTTP/3
  • QUIC
  • Networking
  • Performance

HTTP/2 fixed head-of-line blocking at the HTTP layer — multiple requests share one TCP connection instead of needing one connection each — and then TCP itself turned out to be the thing still blocking everyone.

HTTP/3’s actual change is one layer down: it drops TCP entirely and runs over QUIC, a transport built on UDP that implements its own reliability and ordering. Everything people find surprising about HTTP/3 — why it helps phones more than datacenters, why some corporate networks can’t use it at all, why it costs more CPU — follows from that one decision.

Why did HTTP/2 stop being enough?

Because TCP delivers a byte stream, and a byte stream has exactly one order.

HTTP/2 multiplexes many requests into frames and interleaves them down a single connection. TCP underneath knows nothing about frames or streams. It knows it has bytes 1 through 40,000 and it must hand them to the application in order, with no gaps. Lose the packet carrying bytes 12,000–13,400 and TCP will not deliver byte 13,401 to your browser — even though it has it, sitting in a buffer — until the retransmission of that one packet arrives, a full round trip later.

So a stylesheet that was fully received gets held hostage by a lost packet belonging to an analytics beacon. HTTP/2 moved head-of-line blocking from the protocol into the transport rather than removing it.

Figure 1

One lost packet, three transports

HTTP/1.16 TCP connections
styles.css
app.js
hero.webp
beacon.json

HTTP/21 TCP connection
styles.css
app.js
hero.webp
beacon.json

HTTP/31 QUIC connection
styles.css
app.js
hero.webp
beacon.json

Drop a packet belonging to
Four responses share a connection. HTTP/1.1 opens one TCP connection per response, so a loss affects only its own — but pays a handshake each time. HTTP/2 coalesces onto one TCP connection, so TCP's single ordered byte stream makes every response wait for the retransmission. HTTP/3 keeps the one connection and gives each response its own ordering, so only the affected one waits. A crude model: 40 ms round trip, 4 packets per response, one round trip to recover a loss.

The comparison people usually skip is the left column. HTTP/1.1 with six parallel connections is also resistant to this, because six TCP connections have six independent byte streams. That is not an argument for HTTP/1.1 — six connections means six handshakes, six congestion windows each restarting from slow start, and no header compression — but it does tell you something precise: HTTP/2’s regression under loss is the price it paid for connection coalescing, and HTTP/3 is the attempt to stop paying it.

What is QUIC doing instead?

QUIC moves the reliability machinery up out of the kernel and makes the stream the unit of ordering rather than the connection. A QUIC connection carries numbered streams, each with its own delivery guarantee and its own flow-control window. A lost packet blocks only the streams whose data it carried.

Three consequences fall out of that, and all three are the actual reason HTTP/3 exists:

Loss is contained. One dropped packet stalls one response, not all of them. This is the whole point, and it is why the benefit scales with your loss rate — at 0% loss, the mechanism has nothing to do.

Headers get their own fix. HTTP/2’s HPACK compression is stateful: a header block can reference entries added by an earlier one, so a lost header frame stalls later header frames even under QUIC. HTTP/3 replaces it with QPACK, which lets a request opt out of referencing not-yet-acknowledged table entries and take a slightly worse compression ratio in exchange for not being blocked.

The transport stops being ossifiable. QUIC encrypts almost its entire header, including packet numbers. Middleboxes cannot read it, so they cannot “helpfully” rewrite it — which is precisely why TCP’s own extension mechanisms have been frozen for two decades. This is an unglamorous benefit that will matter more over the next ten years than anything on your dashboard today.

What the handshake saves

A fresh HTTPS connection over TCP costs two round trips before the client may send a request: one for TCP’s SYN/SYN-ACK, one for the TLS 1.3 handshake. They are strictly sequential, because TLS runs inside the TCP connection.

QUIC has no such layering. The crypto handshake is carried in QUIC’s own packets, so transport and TLS complete together in one round trip.

Figure 2

Round trips before the first request byte

TCP + TLS 1.3
TCP SYN / SYN-ACK
TLS 1.3 handshake

Strictly sequential: TLS runs inside the TCP connection, so it cannot start until TCP is up.

QUIC, 1-RTT
QUIC + TLS together

The crypto handshake is carried in QUIC's own packets, so transport and TLS finish in the same trip.

QUIC, 0-RTT
no setup — the request is the first packet

Only on a resumed connection, and only safe for requests you would not mind an attacker replaying.

Round trip
TCP plus TLS 1.3 costs two round trips of setup; QUIC folds them into one; 0-RTT resumption spends none but cannot be protected against replay. The number of round trips saved is fixed — what changes with the link is what each one costs, which is why the same protocol change is decisive on a phone and irrelevant inside a datacenter.

Notice what the slider does to the argument. At 20 ms — a wired connection to a nearby edge — you are arguing over 20 milliseconds, which no user will ever perceive. At 220 ms — a mobile connection to a distant origin — the same single saved round trip is a fifth of a second, before your application has done anything at all. The mechanism is identical in both cases; only the multiplier changed. Almost every disagreement about whether HTTP/3 is worth it is really a disagreement about which multiplier you are looking at.

What 0-RTT actually promises

QUIC can go further. If the client has connected before, it can cache the server’s parameters and send an encrypted request in its very first packet — zero round trips of setup. This is genuinely fast and genuinely dangerous, and the danger is not hypothetical.

A 0-RTT packet cannot be protected against replay. The server has not yet proved liveness, so an attacker who captures that packet can send it again later and the server has no way to distinguish the copy from the original. If the request was GET /article/12, replaying it is harmless. If it was POST /transfer, it is not.

So the rule is not “0-RTT is unsafe”, it is:

  • Only idempotent requests belong in 0-RTT. In practice this means safe methods, and it means your CDN or server must be configured to enforce it rather than trusting clients. Most edge providers restrict 0-RTT to GET and require you to opt in.
  • Anything reading a session cookie needs care even when idempotent. A replayed authenticated GET can still be used as an oracle — it reveals timing and cache state about a real user’s session.
  • The saving is one round trip, once, per connection. It is the smallest of HTTP/3’s wins and carries the largest footgun. Turn it on last, if at all.

The feature nobody advertises

TCP identifies a connection by the four-tuple of source IP, source port, destination IP and destination port. Change any of them and it is, definitionally, a different connection — which is why walking out of your house and dropping off WiFi onto cellular kills every in-flight request and starts every handshake again.

QUIC identifies a connection by a connection ID that the endpoints choose, carried in the packets themselves. The IP can change underneath and the connection survives; the server sees packets with a familiar connection ID from a new address, validates the new path, and carries on. Your upload does not restart.

If your traffic is mostly phones — and for most consumer products it is — this may quietly be the largest real-world win on the list, and it never shows up in a synthetic benchmark run on a wired laptop.

Where does it not help?

Honesty about the failure cases is what separates a decision from a fashion.

UDP is blocked more often than you would like. Plenty of corporate and hotel networks drop UDP/443 outright. HTTP/3 is not something a browser assumes; it is advertised via an Alt-Svc response header or an HTTPS/SVCB DNS record, tried opportunistically, and abandoned in favour of HTTP/2 when it doesn’t work. This means you cannot drop HTTP/2 support — you are adding a protocol, not replacing one.

It costs more CPU per byte. TCP lives in the kernel with a decade of offload engineering behind it. QUIC lives in userspace, does its own loss recovery and encrypts every packet header individually. Server-side CPU per gigabyte is measurably higher, and if you terminate your own TLS at scale that shows up on the bill.

Zero loss means zero benefit from the headline feature. A server-to-server call inside one datacenter, over a 0.5 ms link with no loss, making three requests, gains nothing that matters. Head-of-line blocking was never the bottleneck there.

It does not make your application faster. If your server takes 800 ms to render a page, saving one 30 ms round trip changes a 830 ms wait into an 800 ms wait. Transport improvements are bounded by the share of the total that was ever transport.

So should you turn it on?

Figure 3

Should you turn it on?

Expected benefit

Who are your clients?
What does packet loss look like?
How many requests per page?
Where does the time actually go?
HTTP/3's mechanisms only pay out where they have something to work on: loss to contain, round trips to save, network time to shorten. Answer honestly and the same widget will tell you it is not worth the CPU — which for a server-to-server call on a clean link is the correct answer.

For most people the answer is “yes, and it takes an afternoon”, because it is a toggle at the edge rather than a change to your application. Cloudflare, Fastly, Akamai and CloudFront all terminate HTTP/3 for you and speak HTTP/1.1 or HTTP/2 to your origin, which means you get the client-side benefits — the handshake, the loss containment, the connection migration — without touching your stack at all.

The reason to be deliberate about it is not risk, it is expectation-setting. Turn it on, then look at p95 and p99 rather than the median, because that is where the mechanism operates: HTTP/3 mostly does not make the good case better, it makes the bad case less bad. A team that measures the median, sees no change, and concludes the protocol is marketing has measured the wrong number.

The short version

HTTP/3 is HTTP over QUIC instead of TCP, and every difference follows from that. Loss stalls one stream instead of all of them. Setup takes one round trip instead of two. Connections are named by an ID rather than by an IP, so they survive a change of network. The headers get a compression scheme that can decline to block.

It earns its keep in proportion to how bad your clients’ networks are: large wins for mobile users on lossy links to distant origins, marginal wins on a clean wired connection, none at all for the part of the wait your own server is responsible for. Turn it on at your edge, leave HTTP/2 in place underneath it because some networks will refuse UDP, look at your tail latencies rather than your median, and leave 0-RTT switched off until you can say out loud which of your endpoints are safe to replay.

Next step

Want This Built, Not Just Explained?

CodeBrewerz builds the systems these posts take apart: web, mobile, cloud and the infrastructure underneath. Tell us what you are building.

Start a conversation