HTTP/1.1 was designed in 1999 and optimized for a world of dial-up connections, small objects,
and one request per connection. The web of 2026 has 50 Mbps connections loading 5 MB of
resources across 100 requests. HTTP/1.1's model — one request at a time per connection, with
keep-alive to avoid re-establishing TCP — is the wrong answer to this problem. HTTP/2 and
HTTP/3 fix the model at different layers: HTTP/2 fixes the multiplexing problem inside the
connection, and HTTP/3 fixes the transport problem underneath.
The HTTP/1.1 bottleneck: head-of-line blocking
HTTP/1.1 uses one request per TCP connection at a time. keep-alive reuses the TCP
connection for multiple requests, but requests are still serialized:
HTTP/1.1 with keep-alive:
request 1 ──────→ response 1 ──→ request 2 ──→ response 2 ──→ ...
(blocked) (blocked)To work around this, browsers open 6 TCP connections per origin (the "connection pool"). Each connection carries one request at a time. But 6 connections means 6 TCP handshakes, 6 TLS handshakes, and 6 independent congestion windows — each starting in slow start.
The result: a page with 100 resources needs 6 connections, each carrying ~17 requests serially. The total latency is the sum of all requests on the slowest connection.
HTTP/2: multiplexing inside one connection
HTTP/2 solves head-of-line blocking at the HTTP layer by multiplexing multiple requests over a single TCP connection:
HTTP/2:
connection 1:
┌─ request 1 (stream 1) ──→ response 1
├─ request 2 (stream 3) ──→ response 2
├─ request 3 (stream 5) ──→ response 3
└─ (interleaved, concurrent)Each request is a stream. Streams are independent — a slow response on stream 1 does not block stream 3. The TCP connection carries all streams simultaneously, and the server can prioritize which streams get bandwidth.
What HTTP/2 fixes:
-
Multiplexing. 100 requests over one TCP connection instead of 6 connections × 17 requests each. One TCP handshake, one TLS handshake, one congestion window.
-
Header compression (HPACK). HTTP/1.1 sends full headers on every request. HTTP/2 compresses headers using a dynamic table — repeated headers (cookies, user-agent, accept) are sent as single-byte references.
-
Server push. The server can send responses before the client requests them. If the server knows the client will need
style.cssafterindex.html, it can pushstyle.cssproactively. (In practice, server push is rarely used — it's hard to get right and often wastes bandwidth.) -
Stream prioritization. The client can tell the server which streams are more important. A CSS file can be prioritized over an image, ensuring the critical rendering path is fast.
What HTTP/2 does not fix:
TCP head-of-line blocking. If one TCP packet is lost, TCP stalls all streams on that connection — because TCP guarantees byte-stream order, and it cannot deliver stream 3's data until stream 1's lost packet is retransmitted. A single packet loss stalls every stream on the connection.
packet loss on HTTP/2 connection:
stream 1: ──→ [lost packet] ──→ stalled
stream 2: ──→ [waiting for stream 1's retransmit]
stream 3: ──→ [waiting for stream 1's retransmit]
stream 4: ──→ [waiting for stream 1's retransmit]
all streams blocked by one lost packetThis is why HTTP/2 over a lossy connection can be worse than HTTP/1.1 with multiple connections — one loss stalls everything instead of just one request.
HTTP/3: QUIC replaces TCP
HTTP/3 runs on QUIC (RFC 9000) instead of TCP. QUIC is a UDP-based transport that provides:
- Reliable, ordered delivery (like TCP)
- Built-in TLS 1.3 (encryption is mandatory, not optional)
- Per-stream ordering (no head-of-line blocking across streams)
- Connection migration (survives IP address changes)
HTTP/2 over TCP:
TCP connection → carries all streams → one loss blocks all
HTTP/3 over QUIC:
QUIC connection → carries all streams
stream 1: ──→ [lost packet] ──→ stalled
stream 2: ──→ [independent, continues] ──→ response
stream 3: ──→ [independent, continues] ──→ responseWhen a packet is lost on a QUIC connection, only the streams that depend on that packet are stalled. Other streams continue unaffected. This is the fundamental improvement: head-of-line blocking is per-stream, not per-connection.
Connection migration: QUIC connections are identified by a connection ID, not a 4-tuple (source IP, source port, dest IP, dest port). When a mobile device switches from Wi-Fi to cellular (IP address change), a TCP connection dies and must be re-established. A QUIC connection survives — the connection ID is unchanged, and the client sends a path validation probe to the server.
TCP on network switch:
Wi-Fi connection (192.168.1.100:443 → 10.0.0.1:443)
→ switch to cellular
→ new IP (10.42.0.50:443 → 10.0.0.1:443)
→ TCP connection is dead (different 4-tuple)
→ must re-establish: 1-3 RTTs
QUIC on network switch:
Wi-Fi connection (connection ID: 0xabc123)
→ switch to cellular
→ new IP, same connection ID
→ 0 RTT resume (or 1 RTT path validation)The TLS advantage: 0-RTT and 1-RTT
QUIC integrates TLS 1.3 directly — the handshake is part of the transport handshake:
QUIC handshake (first connection):
client → server: Initial (ClientHello, key share)
server → client: Handshake (ServerHello, certificates, finish)
total: 1 RTT (vs 2-3 RTT for TCP + TLS 1.2)
QUIC 0-RTT (resumed connection):
client → server: Initial + 0-RTT data (encrypted with cached key)
server → client: Handshake + response
total: 0 RTT for the first data packetTCP + TLS 1.2 costs 3 RTTs (1 for TCP handshake, 2 for TLS). TCP + TLS 1.3 costs 2 RTTs (1 for TCP, 1 for TLS). QUIC costs 1 RTT on first connection and 0 RTT on resumption.
On a 50ms RTT connection, that's 150ms saved on the first request and 100ms saved on every subsequent request. For mobile users on high-latency cellular connections, this is transformational.
The adoption reality
| Protocol | Browser support | Server support | CDN support |
|---|---|---|---|
| HTTP/1.1 | 100% | 100% | 100% |
| HTTP/2 | 100% | 95%+ | 100% |
| HTTP/3 | 95%+ (Chrome, Firefox, Edge) | 70%+ (nginx, Caddy, LiteSpeed) | 100% (Cloudflare, Fastly, Akamai) |
If you're behind a CDN (Cloudflare, Fastly, Akamai), HTTP/3 is already enabled by default. The CDN terminates QUIC and communicates with your origin over HTTP/2 or HTTP/1.1. You get HTTP/3 benefits for clients without changing your origin server.
If you're not behind a CDN, nginx 1.25+ supports HTTP/3 with quic module. Caddy supports
HTTP/3 by default. Go's net/http experimental QUIC support is maturing.
What this means for your code
-
Use HTTP/2 multiplexing. Combine small resources, use a single connection, and let the protocol handle concurrency. The "6 connection pool" hack is obsolete.
-
Enable HTTP/3 via your CDN. Cloudflare, Fastly, and Akamai enable it by default. You get 0-RTT, connection migration, and per-stream ordering for free.
-
Don't use server push. It's unreliable, hard to debug, and
103 Early Hintsis a better alternative — the server sends hint headers while the origin generates the full response. -
Optimize for 0-RTT. If your application uses HTTP/3, ensure your critical GET requests are safe to replay. 0-RTT data can be replayed, and non-idempotent requests must not be sent in 0-RTT.
-
Measure with real networks. HTTP/3's advantage is most visible on lossy, high-latency connections (mobile). Test on cellular networks, not just your office Wi-Fi.