A

autochitect

Explore software architecture ◆

HTTP/2 & HTTP/3

2026-05-22NetworkingProtocolsPerformanceArchitecture

How HTTP evolved from a text protocol with one request per connection to a multiplexed binary stream, and then to a transport built on UDP that survives network switches.

HTTP/2 & HTTP/3

Think of HTTP/1.1 like a single-lane checkout: one item is processed at a time, and the customer ahead of you with a full cart makes you wait even if you only have one item. HTTP/2 opened multiple lanes. HTTP/3 rebuilt the entire store on a faster road system.

Both revisions were driven by the same pressure: the web got heavier. A typical web page in 1995 was a single HTML file. A typical page in 2025 fetches dozens of assets — HTML, JavaScript bundles, CSS, fonts, API calls, images. HTTP/1.1 was designed for the former and shows its age at the latter.

HTTP/1.1: The Bottlenecks

HTTP/1.1 (RFC 2616, 1999; refined RFC 7230–7235, 2014) improved on 1.0 by introducing persistent connections — keep-alive — so a TCP connection could serve multiple requests without re-handshaking. But it kept one constraint: strict request/response ordering. A client sends request 1, waits for response 1, then sends request 2. Head-of-line blocking at the HTTP layer.

Browsers worked around this with up to six parallel TCP connections per origin. Six connections means six sets of TCP handshakes, six TLS negotiations, six congestion-control windows starting cold. It also means multiplexing is done outside the protocol, by opening more sockets.

Headers are the other problem. Every HTTP/1.1 request re-sends all headers — User-Agent, Accept-Encoding, Cookie — in plain ASCII, even if they are identical to the previous request. A typical request header block is 400–800 bytes. On a page that makes 80 requests, that is 32–64 KB of redundant header bytes on every page load.

HTTP/2: Binary Framing and Multiplexing

HTTP/2 (RFC 9113) replaced HTTP/1.1's text protocol with a binary framing layer that splits all communication into small, typed frames. This change enables three things that are impossible in a text-based protocol.

Streams and Multiplexing

A single TCP connection is divided into streams — independent, bidirectional sequences of frames. Each request/response pair occupies its own stream. Streams are identified by an integer ID (client-initiated streams use odd IDs; server-initiated use even IDs).

Connection (single TCP socket)
│
├── Stream 1  →  GET /index.html         ← concurrent
├── Stream 3  →  GET /app.js             ← concurrent
├── Stream 5  →  GET /styles.css         ← concurrent
└── Stream 7  →  GET /logo.png           ← concurrent

Frames from different streams are interleaved on the wire, then reassembled by the receiver. A slow response on stream 1 does not prevent stream 3 from making progress. The browser needs only one TCP connection per origin instead of six.

HPACK: Header Compression

HPACK (RFC 7541) compresses headers using two mechanisms:

  1. Static table — a fixed list of 61 common header/value pairs (e.g., entry 2 is :method: GET). Sending index 2 instead of method: GET costs 1 byte instead of 11.
  2. Dynamic table — a per-connection table that grows as new headers are sent. Once a header has been transmitted once, subsequent requests send only its index.

In practice, repeated requests to the same origin send headers in 10–30 bytes instead of 400–800 bytes. This matters most for APIs making hundreds of small calls — auth headers, cookies, and Accept types are sent once and referenced by index thereafter.

Frame Types

HTTP/2 defines 10 frame types. The ones architects encounter:

Frame Purpose
HEADERS Carries the compressed header block (starts a stream)
DATA Carries the request or response body
PRIORITY Hints at stream processing order (deprecated in RFC 9113)
RST_STREAM Cancels a stream without closing the connection
SETTINGS Negotiates connection parameters (max frame size, initial window)
PUSH_PROMISE Server announces a resource it will push proactively
GOAWAY Gracefully closes the connection
WINDOW_UPDATE Flow control: increase the receive window for a stream or connection

Flow Control

TCP has its own flow control (the receive window), but HTTP/2 adds per-stream flow control on top. Each stream has an independent window. A slow consumer on one stream cannot stall other streams — it just causes the server to pause DATA frames on that stream until the client sends a WINDOW_UPDATE.

Server Push

HTTP/2 allows a server to proactively send resources the client will need before it asks. When responding to GET /index.html, the server can push app.js and styles.css using PUSH_PROMISE frames — the client gets the assets before parsing the HTML and discovering the <script> tags.

In practice, server push saw limited adoption and was deprecated in Chrome. Preload hints (<link rel="preload">) and early hints (103 status code) proved more predictable because the client controls what it receives.

The Problem HTTP/2 Did Not Solve

HTTP/2 eliminated head-of-line blocking at the HTTP layer. It did not eliminate it at the TCP layer.

TCP guarantees that bytes arrive in order. If a packet is lost, TCP holds back all subsequent packets until the lost packet is retransmitted and delivered. This means a single dropped packet stalls every stream on the connection — not just the stream that owns that packet.

TCP stream (HTTP/2 on a single TCP connection)

Packet 1: Stream 1 frame   ✓
Packet 2: Stream 3 frame   ✓
Packet 3: Stream 5 frame   ✗  ← lost, retransmitted
Packet 4: Stream 1 frame   ⏸  ← held back by TCP until packet 3 arrives
Packet 5: Stream 3 frame   ⏸  ← held back

On high-latency or lossy networks (mobile, satellite, cross-ocean), this TCP head-of-line blocking erases much of HTTP/2's multiplexing benefit. The fix required changing the transport layer.

HTTP/3: QUIC as the Transport

HTTP/3 (RFC 9114) moves HTTP from TCP to QUIC (RFC 9000), a new transport protocol built on UDP. QUIC implements reliability, flow control, and congestion control in userspace, but does so at the stream level rather than the connection level.

Stream-Level Reliability

In QUIC, each stream handles its own retransmission independently. A lost packet only blocks the stream that owns it — other streams continue flowing.

QUIC connection (over UDP)

Stream 1: [frame 1] [frame 2]      [frame 4]  → delivered in order
Stream 3: [frame 1]          ✗             → retransmitting frame 2
Stream 5: [frame 1] [frame 2] [frame 3]    → unaffected

This is the fundamental improvement over HTTP/2 on TCP.

Built-in TLS 1.3

HTTP/2 requires TLS as a practical matter (browsers only implement h2 over TLS). TLS and TCP are separate layers — the TLS handshake occurs after the TCP handshake, adding at minimum 1 RTT.

QUIC integrates TLS 1.3 into its own handshake. A new QUIC connection establishes both transport and encryption in 1 RTT. A resumed connection (session ticket) completes in 0 RTT — the client can send application data in the first packet, before the server responds.

HTTP/1.1 (new connection):
  TCP SYN → SYN-ACK → ACK          (1 RTT)
  TLS ClientHello → ServerHello…   (1 RTT, TLS 1.3)
  HTTP request                     → 2 RTT before data

HTTP/3 (new connection):
  QUIC Initial (crypto + transport) (1 RTT)
  HTTP request                     → 1 RTT before data

HTTP/3 (resumed connection, 0-RTT):
  QUIC Initial + HTTP request      (0 RTT)
                                   → data arrives in first round trip

Connection Migration

TCP connections are identified by a 4-tuple: (source IP, source port, destination IP, destination port). When a mobile client moves from Wi-Fi to cellular, its IP changes, the 4-tuple becomes invalid, and every TCP connection drops. The client must reconnect, re-negotiate TLS, and resume any in-flight requests.

QUIC connections are identified by a Connection ID — an opaque byte string chosen by the client. When the network path changes, the client sends a new source IP/port but the same Connection ID. The server recognises the migration and continues without interruption. HTTP/3 downloads survive a network switch; HTTP/2 downloads restart.

QPACK: Header Compression for QUIC

HPACK was designed assuming in-order delivery — the dynamic table state is updated sequentially. QUIC streams are independent and can deliver headers out of order. Applying HPACK directly would cause decompression failures when a header block referencing the dynamic table arrives before the frame that updated it.

QPACK (RFC 9204) solves this with a two-stream encoder architecture: a dedicated "encoder stream" and "decoder stream" synchronize the dynamic table state independently of the request streams. Request headers can still reference the dynamic table, but only after the required table entries have been acknowledged.

HTTP/1.1 vs HTTP/2 vs HTTP/3 at a Glance

Property HTTP/1.1 HTTP/2 HTTP/3
Transport TCP TCP QUIC (UDP)
Protocol encoding Text Binary frames Binary frames
Connections per origin 6 (browsers) 1 1
Multiplexing No (per connection) Yes (streams) Yes (streams)
Header compression None HPACK QPACK
HOL blocking (HTTP) Yes No No
HOL blocking (transport) Yes Yes No
TLS Optional Required (browsers) Integrated
Handshake cost (new) 2 RTT (+ TLS) 2 RTT (+ TLS 1.3 1 RTT) 1 RTT
Handshake cost (resumed) 2 RTT 1 RTT (TLS session) 0 RTT
Connection migration No No Yes
Server push No Yes (deprecated) No (removed)

When to Use Which

Stick with HTTP/2 when:

  • You control internal microservice communication — gRPC over HTTP/2 is the standard.
  • Your load balancer or reverse proxy (NGINX, HAProxy) doesn't yet support QUIC.
  • Clients are non-browser (CLIs, server-to-server) and connection migration is irrelevant.

Move to HTTP/3 when:

  • You serve mobile clients on variable networks — connection migration alone justifies the upgrade.
  • Your API makes many small, latency-sensitive requests where 0-RTT resumption saves measurable time.
  • You operate a CDN edge — all major CDN providers (Cloudflare, Fastly, AWS CloudFront) support HTTP/3.
  • Packet loss is a real concern (international traffic, satellite links, congested Wi-Fi).

HTTP/3 deployment note: QUIC runs on UDP port 443. Many corporate firewalls block or rate-limit UDP, causing QUIC connections to time out. Clients fall back to HTTP/2 via the Alt-Svc header, so the failure mode is graceful — but expect 2–5% of users on restrictive networks to never see HTTP/3.

Deployment in Practice

The Alt-Svc response header is how a server advertises HTTP/3 support to clients. An HTTP/2 response includes:

Alt-Svc: h3=":443"; ma=86400

The client stores this hint (max-age 86400 seconds) and uses QUIC on the next connection to the same origin. The first connection to a new origin always uses HTTP/1.1 or HTTP/2 — there is no way to know HTTP/3 is available before contacting the server. Tools like HTTPS DNS records (Alt-Svc in DNS) are emerging to solve this.

Negotiation uses ALPN (Application-Layer Protocol Negotiation, RFC 7301) — a TLS extension that lets client and server agree on the application protocol during the TLS handshake. The client sends ["h2", "http/1.1"] in the ClientHello; the server picks the highest it supports.

References

RFC Title Year
RFC 7541 HPACK: Header Compression for HTTP/2 2015
RFC 9000 QUIC: A UDP-Based Multiplexed and Secure Transport 2021
RFC 9001 Using TLS to Secure QUIC 2021
RFC 9002 QUIC Loss Detection and Congestion Control 2021
RFC 9113 HTTP/2 2022
RFC 9114 HTTP/3 2022
RFC 9204 QPACK: Field Compression for HTTP/3 2022