HTTP/2 vs HTTP/3: Multiplexing, QUIC, and Real-World Performance
HTTP/2 vs HTTP/3 compares streams, head-of-line blocking, and QUIC so you can pick a protocol that stays fast when packets drop on real networks today.
HTTP/2 vs HTTP/3 is a choice about what happens when a packet is lost. When many calls share one connection, a stall on that connection becomes a stall for the user. You can have a perfect origin and still see a slow tail. Still, teams turn a protocol flag on and expect the tail to vanish.
Both versions multiplex streams, so one connection carries many requests. However, they repair loss in different places. Because of that, the same drop can freeze every stream or only one. Also, some networks still block the UDP path that HTTP/3 needs, so a fallback plan is part of the design.
What each version is and why it fails
RFC 9113 defines HTTP/2 as binary frames on one TCP connection. Many streams share that pipe, and header compression cuts repeat bytes. When TCP loses a packet, it must fill the hole before later bytes move up. Then every stream waits, even streams whose data already arrived.
RFC 9114 defines HTTP/3 on top of QUIC. RFC 9000 defines QUIC as a transport on UDP with its own loss repair per stream. If one packet drops, other streams can still deliver. Therefore the tail under loss is the main reason to care, not the lab download of one large file.
HTTP/2 fails when you put too much hope in one connection. A common mistake I have seen is a single long lived connection from a busy proxy to an origin. Then one loss, or one huge response, slows a crowd of users. After you add more connections, you trade the stall for more handshake and more state.
HTTP/3 fails when UDP never arrives. Corporate firewalls, old middleboxes, and some load balancers drop it or mishandle it. Then clients wait on a black hole before they fall back to HTTP/2 or TCP. So a naive rollout can make the first request slower, which is the opposite of the goal.
How multiplexing is built
In HTTP/2, a stream is a set of frames with an id. First, the client sends headers. Next, it may send data.
Finally, it reads the response frames for that id. While other streams do the same, TCP still orders the underlying bytes. Because the order is global, loss is global too.
HPACK compresses headers against a table both sides share. If a proxy reorders or coalesces badly, the table can get out of sync and the connection dies. Then every call on it fails at once. Also, a huge cookie or auth header still costs, even when the table hits.
What QUIC changes
QUIC encrypts almost the whole packet and runs many streams inside it. Loss repair is per stream, so a hole in one download does not freeze a sibling API call. Also, connection ids let the same session survive a network change, such as a phone moving from wifi to cellular. When that works, you skip a fresh handshake on the new path.
The handshake is tied to TLS 1.3. Read TLS before you tune session tickets, because 0-RTT data can be replayed. If you send a payment or any non idempotent call in early data, a replay can double it. Therefore keep 0-RTT for safe reads, or disable it until the app can detect a replay.
How clients discover HTTP/3
A client often learns about HTTP/3 from an Alt-Svc header, or from an HTTPS record in DNS. That means the first visit may be HTTP/2, and later visits try QUIC. When the record is wrong or cached too long, clients keep missing the new protocol. DNS for backend engineers covers how long that lie can live.
Some stacks race the two protocols. If UDP is slow to fail, the race still waits. Then the winner is the path that answers, not the path you prefer. Also, a load balancer that understands TCP only will black hole QUIC unless you front it with a UDP capable edge.
Trade-offs for real traffic
Prefer HTTP/2 when your path is stable, UDP is blocked, or your estate is mostly server to server inside a VPC. Prefer HTTP/3 when users are on lossy or mobile networks and you can fall back cleanly. Also, prefer HTTP/2 if your middle tier cannot terminate QUIC yet. If you only serve large files on a clean link, the gap may be too small to matter.
| First choice. | When it fits. | If it breaks. |
|---|---|---|
| HTTP/2 on TCP. | Use it when UDP is blocked or the path is a quiet data center. | One lost packet can stall every stream on the connection. |
| HTTP/3 on QUIC. | Use it when loss or a network change is common. | A blocked UDP path adds delay until fallback runs. |
| Many HTTP/2 connections. | Use them when one pipe is a hot spot. | You pay more handshakes, memory, and file descriptors. |
| 0-RTT early data. | Use it when the request is safe to repeat. | A replay can apply a write twice. |
Server push was part of the HTTP/2 story and is now widely unused. Browsers limit it, and caches get confused. Instead, send the data the client asked for, and let it pipeline the next calls. Also, priority schemes in early HTTP/2 were complex and often ignored, so do not bet latency on a hint your peer skips.
The same transport choice shows up under gRPC and REST. Many gRPC deployments are still HTTP/2, because the ecosystem grew there first. Meanwhile, browser and edge traffic is where HTTP/3 earns its keep. TCP and UDP is the layer under both, and it explains the stall you are trying to remove.
Pitfalls and failure modes
A single shared connection is a blast radius. However, a pool that is too large wastes ports and memory. You want enough pipes to isolate loss, and few enough to reuse handshakes. When every request opens a new connection, you threw away the reason multiplexing exists.
Fallback bugs are the HTTP/3 incident I trust least. If the client waits for QUIC to time out before TCP starts, the first page is slow for users behind a UDP filter. Still, if you disable fallback, those users get nothing. Because of that, race the transports or remember a recent failure and skip QUIC for a while.
Load balancers that only balance TCP will stick a QUIC session to the wrong place, or drop it. When the connection id changes, a naive hash can send packets to a new pod that has no session. Then the user sees a reset mid call. Also, idle timeouts on the balancer must exceed quiet gaps, or the next packet starts a dead session.
- First, confirm which protocol the client actually negotiated, not which one you intended.
- Next, check whether UDP reaches the edge from the networks you care about.
- Then compare tail latency with loss, not only the median on a clean link.
- After that, inspect one connection and see if many calls stalled together.
- Finally, test fallback so a blocked QUIC path cannot add a full timeout.
Header bugs still take down HTTP/2 connections. A peer that sends a bad compressed block forces a connection error. If your gateway shares that connection widely, one bad client header can reset a crowd. Therefore isolate untrusted peers onto their own connections.
A listener setup you can copy
The snippet below is a sketch of an edge that offers both protocols. It is not a full config, and the directives vary by server. Also, the Alt-Svc value tells clients they may try HTTP/3 on the same port next time. If your UDP listener is down, do not advertise it.
listen 443 quic reuseport
listen 443 ssl
http2 on
http3 on
add_header alt-svc 'h3=":443"; ma=86400'
ssl_protocols TLSv1.3
Keep the advertised max age short while you roll out. A long max age is a cache of a protocol hint, and a bad hint lingers. Because clients remember it, a rollback is not instant. Then watch error rates for QUIC separately from TCP so you can tell which path regressed.
Inside the mesh, you may stay on HTTP/2 and skip QUIC. The loss rate is low, and the ops cost of UDP on every sidecar is real. When the user edge is the painful hop, terminate HTTP/3 there and speak HTTP/2 inward. Also, keep deadlines on the inner hop so a stall cannot eat the whole request.
Performance, scale, and cost
On a clean network, HTTP/2 and HTTP/3 often look similar for a single large download. The gap shows up with many small calls, loss, or a changing client address. In an illustrative production range, a one percent loss on a long path can move p99 by hundreds of milliseconds on HTTP/2. If your users are mobile, that is the number to chase.
CPU is the other side of the trade. QUIC runs more of the transport in user space, so you may spend more cores per gigabit than with kernel TCP. Meanwhile, you may spend fewer cores on stalled requests and retries. Therefore measure both, or you will celebrate latency and miss the bill.
Scale means connection state, UDP packet rate, and how you balance. A fleet that terminates millions of QUIC sessions needs a plan for connection ids and for worker restarts. Also, a restart drops session state unless you share tickets and ids with care. So drain slowly, and expect a handshake spike after a push.
Cost shows up as edge CPU, support load from networks that block UDP, and the time you spend on fallback. HTTP/2 is cheaper to operate in a locked down VPC. HTTP/3 is worth it when the tail is user visible and you can see protocol metrics. After you enable it, keep a kill switch that stops advertising HTTP/3 without a code change.
Key Takeaways
- Also remember that HTTP/2 multiplexes on TCP, so one loss can stall every stream.
- When HTTP/3 repairs loss per stream, other calls on that connection can still finish.
- Because some networks block UDP, ship a fallback that does not wait a full timeout.
- If you use 0-RTT, keep it off non idempotent writes so a replay cannot double charge.
- Still watch which protocol clients really use, not only the flag you set.
- Therefore terminate QUIC at the edge if the mesh is happier on HTTP/2.
FAQ
Will HTTP/3 always beat HTTP/2?
No, because a clean data center path has little loss to repair. HTTP/2 can match it there and is easier to balance with TCP tools you already run. However, mobile and lossy links are where QUIC usually wins on the tail. Then you still need numbers from your own users before you call it done.
What happens when UDP is blocked?
The client must use HTTP/2 or HTTP/1.1 on TCP, or the request fails. If the stack waits too long to notice, the user pays that wait on the first try. Also, remember the failure so the next request skips QUIC for a while. After the network allows UDP again, you can try it on a later visit.
Should internal RPC move to HTTP/3?
Often it should not, since loss inside the VPC is rare and UDP adds ops work. gRPC on HTTP/2 is the boring path, and boring is a feature. When a specific hop shows head of line stalls, test QUIC there. If the gain is small, keep TCP and spend the time on deadlines and pools.
Why did one bad request reset many calls?
Because they shared one HTTP/2 connection, and a connection error kills every stream. Also, a bad header block or a proxy bug can trigger that error. Then isolate noisy peers, and raise the pool so one reset is not the whole client. Finally, log the connection id with the stream id so the blast radius is obvious.
Turn on protocol metrics for one user facing host and split latency by HTTP/2 and HTTP/3. Compare the tail on lossy networks, and time the fallback when UDP is dropped. After that, advertise HTTP/3 only if the tail improves and the fallback stays quick. If the gain is noise, stay on HTTP/2 and fix connection reuse instead.
Last updated on 17 September 2026.