HTTPhttp/1.1http/2http/3comparisonquic

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.

ConceptualBrowserSimulated
▶ InteractiveInterview question
Progress

The problem

A team is asked "should we enable HTTP/3?" for an internal API, a public website and a mobile app backend. The honest answer is different for each, because the versions differ in exactly the properties that depend on the link: setup cost, behaviour under loss, and what the infrastructure in between can carry.

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.

The three versions on the properties that differ
PropertyHTTP/1.1HTTP/2HTTP/3
TransportTCP (+ TLS on 443)TCP + TLS (browsers require TLS; ALPN h2)QUIC over UDP, TLS 1.3 built in
FramingText lines; Content-Length or chunkedBinary frames with stream idsBinary frames; one request per QUIC stream
MultiplexingNone: one request in flight per connection; ~6 connections per origin in browsersMany streams on one connectionMany streams on one connection
Cold setup to first requestTCP 1 RTT + TLS 1–2 RTTTCP 1 RTT + TLS 1–2 RTT1 RTT combined; 0-RTT resumption
Behaviour under packet lossA loss stalls one of ~6 connectionsA loss stalls every stream (TCP HoL)A loss stalls only the affected stream(s); shared congestion window
Header compressionNone (headers repeated in full)HPACKQPACK
Connection identityIP 4-tuple; breaks on network changeIP 4-tuple; breaks on network changeConnection ID; survives IP change (migration)
Prioritisation / pushClient ordering onlyRFC 9218 priorities; push deprecatedRFC 9218 priorities; no push
Kernel / infrastructureEverywhere; every proxy, LB and toolBroad; L4 balancers pin a client to one backendUserspace; needs UDP/443 open; fewer proxies and tools; higher CPU
Web usageLegacy and non-browser clientsMajority of browser page loadsLarge and growing minority, mostly via CDNs (numbers shift yearly)
Debuggabilitycurl, telnet, plain tcpdumpNeeds h2-aware tools; TLS keys to decryptNeeds QUIC-aware tools (qlog, Wireshark with keys)

The same page under one lost packet

Simulated

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.

Six assets, RTT 80 ms, one packet lost (simulated proportions, not a measurement)
                    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 ms

Choosing

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 vs HTTP/2 vs HTTP/3
What changed between the versions, and a toy page-load model to see when each one wins.
HTTP/1.1HTTP/2HTTP/3
TransportTCPTCPQUIC over UDP
MultiplexingNone — one request in flight per connection; browsers open ~6Many streams on one connection (binary frames)Many streams, each with its own loss recovery
Connection setup1 RTT TCP + 1 RTT TLS 1.3 (per connection)1 RTT TCP + 1 RTT TLS 1.31 RTT (TLS 1.3 built into QUIC); 0-RTT on resumption
Header compressionNone (text, repeated every request)HPACKQPACK
Under packet lossOne connection stalls, the others continueWhole connection stalls (transport HoL)Only the affected stream stalls
Encryption in browsersOptional (http:// allowed)Required in practice (browsers only do h2 over TLS)Required (QUIC is always encrypted)
Typical useSimple APIs, internal tools, curlMost sites today; gRPCCDNs, large sites, mobile and lossy networks
Network
Page
Simulated
HTTP/1.1141 ms · headers 5.9 KB
HTTP/2166 ms · headers 1.1 KB
HTTP/3fastest141 ms · headers 1.1 KB
setuprequest roundstransferloss stalls
HTTP/3 wins here. Few resources, little loss: HTTP/2 and HTTP/3 are close; HTTP/1.1 pays for two extra request rounds. HTTP/3 edges ahead with its 1-RTT setup.
Model: setup = handshake RTTs; request rounds = ⌈resources / connections⌉ × RTT; transfer = max(bytes / bandwidth, slow-start rounds × RTT); loss stalls = expected lost segments × RTT × the share of traffic a stall blocks (1/6 per HTTP/1.1 connection, all of HTTP/2, one stream in HTTP/3). Real pages also depend on priorities, caching and server push — treat the numbers as directions, not measurements.

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.