The Runtime Theory
Networking

How HTTP/2 and HTTP/3 Change Everything

Multiplexing, server push, and QUIC — why keep-alive is the old answer and what the new protocols actually fix.

The Runtime Theory Team4 min read#http2#http3#quic#multiplexing#web-performance
On this page

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:

text
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:

text
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:

  1. Multiplexing. 100 requests over one TCP connection instead of 6 connections × 17 requests each. One TCP handshake, one TLS handshake, one congestion window.

  2. 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.

  3. Server push. The server can send responses before the client requests them. If the server knows the client will need style.css after index.html, it can push style.css proactively. (In practice, server push is rarely used — it's hard to get right and often wastes bandwidth.)

  4. 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.

text
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 packet

This 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)
text
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] ──→ response

When 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.

text
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:

text
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 packet

TCP + 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

ProtocolBrowser supportServer supportCDN support
HTTP/1.1100%100%100%
HTTP/2100%95%+100%
HTTP/395%+ (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

  1. Use HTTP/2 multiplexing. Combine small resources, use a single connection, and let the protocol handle concurrency. The "6 connection pool" hack is obsolete.

  2. 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.

  3. Don't use server push. It's unreliable, hard to debug, and 103 Early Hints is a better alternative — the server sends hint headers while the origin generates the full response.

  4. 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.

  5. 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.