HTTP/1.1 vs HTTP/2 vs HTTP/3
Three versions with identical semantics and three different transports: sequential text over TCP, multiplexed frames over one TCP connection, and multiplexed streams over QUIC — each is the right choice for a different link and a different deployment.
The problem
What changed, and what did not
Methods, status codes, headers, caching, cookies — the semantics in HTTP: Requests, Responses, Headers and Status Codes — are the same across all three versions; an application handler cannot tell which one the client used unless it asks. What changed is the transport binding: how requests are framed, whether they share a connection, what a lost packet costs, and how many round trips it takes to start. Every row in the matrix below is a consequence of the transport choice, not of HTTP.
| Property | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transport | TCP (+ TLS on 443) | TCP + TLS (browsers require TLS; ALPN h2) | QUIC over UDP, TLS 1.3 built in |
| Framing | Text lines; Content-Length or chunked | Binary frames with stream ids | Binary frames; one request per QUIC stream |
| Multiplexing | None: one request in flight per connection; ~6 connections per origin in browsers | Many streams on one connection | Many streams on one connection |
| Cold setup to first request | TCP 1 RTT + TLS 1–2 RTT | TCP 1 RTT + TLS 1–2 RTT | 1 RTT combined; 0-RTT resumption |
| Behaviour under packet loss | A loss stalls one of ~6 connections | A loss stalls every stream (TCP HoL) | A loss stalls only the affected stream(s); shared congestion window |
| Header compression | None (headers repeated in full) | HPACK | QPACK |
| Connection identity | IP 4-tuple; breaks on network change | IP 4-tuple; breaks on network change | Connection ID; survives IP change (migration) |
| Prioritisation / push | Client ordering only | RFC 9218 priorities; push deprecated | RFC 9218 priorities; no push |
| Kernel / infrastructure | Everywhere; every proxy, LB and tool | Broad; L4 balancers pin a client to one backend | Userspace; needs UDP/443 open; fewer proxies and tools; higher CPU |
| Web usage | Legacy and non-browser clients | Majority of browser page loads | Large and growing minority, mostly via CDNs (numbers shift yearly) |
| Debuggability | curl, telnet, plain tcpdump | Needs h2-aware tools; TLS keys to decrypt | Needs QUIC-aware tools (qlog, Wireshark with keys) |
The same page under one lost packet
The rows that matter most in production are setup cost and loss behaviour, and both are easiest to see on one concrete case: a page needing six 30 kB assets from one origin, RTT 80 ms, and exactly one packet lost early in the transfer. The proportions below are an educational model — real outcomes depend on the congestion controller, the loss position, retransmission timers and whether the loss is detected by duplicate ACKs or a timeout — but the shape is what each transport guarantees.
HTTP/1.1 opens six connections, pays six handshakes (in parallel, so two RTTs of wall-clock, but six times the handshake CPU) and the loss stalls one asset while five proceed. HTTP/2 pays one handshake, then the loss stalls *all six* streams for at least one RTT while TCP retransmits, because every stream’s bytes sit behind the hole in one byte stream. HTTP/3 pays one combined round trip and the loss stalls only the stream whose data was in the lost packet — but every stream shares the congestion window, which shrinks on the loss, so the others slow rather than stop.
HTTP/1.1 (6 conns) HTTP/2 (1 conn) HTTP/3 (1 QUIC conn)
setup to 1st byte 160 ms (TCP+TLS1.3) 160 ms (TCP+TLS1.3) 80 ms (combined)
loss at ~200 ms 1 of 6 assets waits all 6 streams wait 1 stream waits, 5 continue
~80 ms for retransmit ~80 ms for retransmit (cwnd shrinks for all)
approx. finish ~ 420 ms ~ 400 ms ~ 300 ms
handshake CPU 6× 1× 1× (+ userspace QUIC cost)
same case, no loss ~ 340 ms ~ 320 ms ~ 240 msChoosing
The choice follows the link. Inside a data centre — sub-millisecond RTT, negligible loss, warm pooled connections — HTTP/1.1 with keep-alive is perfectly adequate and the easiest to debug, proxy and load-balance; HTTP/2 is worth it when the protocol on top wants streams (gRPC requires it) or when a client makes many concurrent small requests to one backend and pooling dozens of h1 connections is awkward. HTTP/3 buys nothing here that justifies its CPU and tooling cost.
Public websites get HTTP/2 from any modern server or CDN for free, and it is the default answer: one connection, compressed headers, no sharding hacks. HTTP/3 shines on lossy, high-latency, mobile last miles — 1 RTT setup, connection migration when the phone changes network, and loss that stalls one image instead of the whole page. The pragmatic deployment is a CDN or edge load balancer that terminates HTTP/3 and HTTP/2 from clients and speaks HTTP/1.1 or /2 to origins over warm connections, so the origin never runs a QUIC stack.
Two constraints override preference. Browsers only speak h2 and h3 over TLS, so a plaintext endpoint is HTTP/1.1 (or h2c for non-browser clients). And anything in the path must support the version: a load balancer that balances TCP connections undermines h2, a firewall that drops UDP/443 removes h3, and an old proxy that buffers whole responses turns any streaming design into a batch one. Measure with real clients on real networks; the win from h3 is a distribution, not a constant.
- Internal, low-latency, reliable links: HTTP/1.1 keep-alive, or HTTP/2 when the protocol needs streams (gRPC).
- Public web: HTTP/2 by default; enable HTTP/3 at the CDN/edge for mobile and long-haul users.
- Mobile apps on cellular: HTTP/3 where the client library supports it and UDP is reachable; fallback to h2 must be automatic.
- Never assume: check what the load balancer, the proxy and the client library actually negotiate (
curl -v,--http3, DevTools Protocol column).
Key points
- All three versions share HTTP semantics; they differ only in the transport binding — framing, multiplexing, setup cost, loss behaviour.
- HTTP/1.1: one request per connection at a time, ~6 browser connections per origin, headers uncompressed, universal support.
- HTTP/2: many streams on one TCP connection with HPACK; a lost segment stalls all streams; requires TLS in browsers; L4 balancers pin clients to one backend.
- HTTP/3: streams over QUIC/UDP with TLS 1.3 built in; 1-RTT setup, per-stream loss recovery, connection migration; needs UDP/443, costs more CPU.
- Internal reliable links: HTTP/1.1 or /2. Public web: HTTP/2 by default. Lossy mobile and long-haul: HTTP/3 at the edge.
- The infrastructure in the path decides what actually gets negotiated; verify rather than assume.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why keep three versions in service instead of retiring the old ones?
Because the older ones are simpler and universally supported: every proxy, tool and embedded device speaks HTTP/1.1, and h1 keep-alive over a clean link has no performance problem worth solving. Each version is an answer to a link condition the previous one handled badly.
▸Why does packet loss decide between HTTP/2 and HTTP/3?
It is the only property where the transports differ qualitatively: TCP’s in-order stream makes one loss cost every h2 stream a retransmission timeout, while QUIC confines it to the affected stream. On a clean link the difference is unobservable.
▸Why terminate HTTP/3 at the edge instead of at the origin?
The lossy, high-RTT segment is the last mile; the edge-to-origin path is a warm, low-loss backbone where h1/h2 pooling is cheaper and easier to operate. You buy the benefit where it exists and skip the cost where it does not.
HTTP/1.1 vs 2 vs 3
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Multiplexing | None — one request in flight per connection; browsers open ~6 | Many streams on one connection (binary frames) | Many streams, each with its own loss recovery |
| Connection setup | 1 RTT TCP + 1 RTT TLS 1.3 (per connection) | 1 RTT TCP + 1 RTT TLS 1.3 | 1 RTT (TLS 1.3 built into QUIC); 0-RTT on resumption |
| Header compression | None (text, repeated every request) | HPACK | QPACK |
| Under packet loss | One connection stalls, the others continue | Whole connection stalls (transport HoL) | Only the affected stream stalls |
| Encryption in browsers | Optional (http:// allowed) | Required in practice (browsers only do h2 over TLS) | Required (QUIC is always encrypted) |
| Typical use | Simple APIs, internal tools, curl | Most sites today; gRPC | CDNs, large sites, mobile and lossy networks |
How it fails
What the failure looks like from inside real software.
- HTTP/3 enabled at the origin behind a load balancer that only forwards TCP: clients advertise h3 via
Alt-Svc, try UDP, time out, fall back — slower than never offering it. - gRPC service behind an L4 balancer: every stream from one client lands on one pod; autoscaling adds pods that get no traffic.
- Internal service migrated from h1 to h2 "for performance" with no measurable gain and a new dependency on h2-aware proxies and debugging tools.
- Old proxy in the path downgrades everything to HTTP/1.1 with
Connection: close; each request now pays a full handshake and nobody notices because the version column in DevTools shows the edge, not the origin. - Header bloat that HPACK hid reappears when a path uses HTTP/1.1: a 6 kB cookie exceeds a proxy’s header limit and returns 431.