RouteHardenHire us
Back to Networking Fundamentals
Networking Fundamentals · Part 10 of 12·Network Hardening··17 min read·intermediate

HTTP/1.1, HTTP/2, HTTP/3 — the evolution

Why HTTP needed three rewrites in twenty years: pipelining's failure, HTTP/2's multiplexing, QUIC's leap to UDP, and the head-of-line blocking that connects all three.

HTTP has been rewritten three times in fifteen years, and the through-line of all three rewrites is the same problem: head-of-line blocking. Every solution that fixes it at one layer reveals it at another. HTTP/1.1 hit it at the application layer; HTTP/2 fixed application-layer HOL but still had transport HOL; HTTP/3 went all the way down and replaced TCP with QUIC to fix transport HOL too. Understanding the evolution is understanding which layer's HOL blocking each version is targeting and which it's still inheriting. This module is the foundational pass: the engineering reasons each version exists, what each version actually changes on the wire, and what to expect when you debug a real HTTP connection in 2026.

Prerequisites

  • Module 1.7 — TCP at the wire level. HTTP/1.1 and HTTP/2 ride on TCP; understanding TCP's byte-stream semantics is the floor for understanding their HOL behavior.
  • Module 1.8 — TCP congestion control. The interaction between connection multiplexing and cwnd is one of HTTP/2's design points.
  • Module 1.9 — DNS, name resolution end to end. HTTP connections start with DNS lookups; HTTPS records (RFC 9460) increasingly steer the protocol negotiation.

Learning objectives

By the end of this module you should be able to:

  1. Explain why HTTP/1.1 pipelining failed to solve application-level concurrency in the real world, and why parallel connections became the de facto workaround.
  2. Compare HTTP/2's multiplexing over one TCP connection with HTTP/3's multiplexing over QUIC streams, and predict how each behaves under packet loss.
  3. Distinguish HPACK (HTTP/2 header compression) from QPACK (HTTP/3 header compression) and explain why the design changed.
  4. Inspect the negotiated HTTP version of a connection with curl -v and relate it to the underlying transport behavior.
  5. Recognize the operational implications — connection setup costs, ALPN, alt-svc, HTTPS records — of which version a deployment is using.

HTTP semantics vs HTTP version mapping

Before talking about versions, it's worth being explicit about what's stable and what's not.

HTTP semantics — methods (GET, POST, PUT, ...), status codes (200, 404, 503, ...), header names and meanings (Cache-Control, Content-Type, Authorization, ...) — have barely changed in twenty years. A 200 OK with a Cache-Control header means the same thing whether it's framed as HTTP/1.1, HTTP/2, or HTTP/3. The application contract between client and server is version-independent.

HTTP version mappings define how those semantics are serialized and transported. HTTP/1.1 maps them to text framing over TCP. HTTP/2 maps them to binary framing over TCP with multiplexing. HTTP/3 maps them to binary framing over QUIC streams. Same semantics, different wire encoding and transport.

This split matters when you read the standards. RFC 9110 defines HTTP semantics. RFC 9112 defines HTTP/1.1's wire format. RFC 9113 defines HTTP/2's. RFC 9114 defines HTTP/3's. They share a top half (semantics) and diverge in the bottom half (transport mapping).

Most application-level code — request handlers, route matchers, middleware — operates entirely on the semantics side and is version-agnostic. The version differences are mostly the concern of the framework's HTTP layer, the load balancer, and the operational stack.

HTTP/1.1's strengths and limits

HTTP/1.1 (RFC 9112, formerly RFC 2616) is text-framed. A request looks like:

GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Accept: */*

A response:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1234
Date: Sun, 03 May 2026 18:00:00 GMT

<...1234 bytes of body...>

The wire format is human-readable, which made it easy to debug and easy to write tools for. The framing is line-based: headers end at \r\n\r\n, the body length is given by Content-Length or chunked-transfer encoding.

The strengths:

  • Simplicity. Anyone can write an HTTP/1.1 client in 50 lines of code.
  • Debuggability. nc example.com 80, type a request manually, see the response.
  • Persistent connections. A single TCP connection can carry multiple request/response pairs sequentially. Reduces TCP setup cost relative to HTTP/1.0's connection-per-request.

The structural limit: HTTP/1.1 is strictly serial on a single connection. Once a request goes out, the response comes back before the next request can start. If the server is slow on one request, every subsequent request on the connection waits.

The standard introduced pipelining as the fix: queue multiple requests on the same connection without waiting for the prior response. The server processes them in order and returns responses in order. This sounds great in theory and was a disaster in practice.

The reasons pipelining failed deployment:

  • Many proxies and middleboxes mishandle pipelined requests. Some return responses out of order; some lose them; some drop the connection. Browsers stopped trusting pipelining as a result.
  • Pipelining doesn't help with HOL blocking. If response 1 is slow, response 2 still waits, even if it would've been fast.
  • The benefit was small. Most of the per-request cost is TLS handshake, which persistent connections amortize anyway.

So browsers gave up on pipelining and adopted multiple parallel TCP connections to the same origin instead — typically 6 connections per origin. This worked but added cost: 6× the TCP setup, 6× the TLS handshakes, 6× the cwnd warm-up. For a page loading 100 resources, the multiplexing was crude and slow.

This is the world HTTP/2 was designed for: parallelism through hacks, with all the costs that came with them.

HTTP/2's design goals

HTTP/2 (RFC 7540, replaced by RFC 9113 in 2022) makes one big design choice: replace text framing with binary frames, enabling multiplexing over a single TCP connection.

The wire is now a stream of frames. Each frame has:

  • A 9-byte header: length, type, flags, stream identifier.
  • A payload up to 2^14 - 1 bytes (the default; can be negotiated up to 2^24 - 1).

Frame types you'll see in captures:

  • HEADERS — request or response headers.
  • DATA — request or response body bytes.
  • SETTINGS — connection-level configuration (initial window size, max concurrent streams, etc.).
  • WINDOW_UPDATE — flow-control credits.
  • PING — keepalive.
  • GOAWAY — graceful shutdown.

Each frame is tagged with a stream ID. Streams are independent logical channels within the single connection. A request and its response share a stream ID; multiple requests use different stream IDs and can be multiplexed in flight simultaneously.

The structural payoff: a single TCP connection can carry hundreds of concurrent requests. No more 6-parallel-connections workaround. One TLS handshake. One cwnd. Frames from different streams interleave on the wire.

HTTP/2 also added:

  • Server push. The server can proactively send resources the client will need (<link rel="preload">-style). Adoption was poor; most modern HTTP/2 servers have disabled push or it's been removed in HTTP/3.
  • HPACK header compression. A compression scheme tailored to HTTP headers. Compresses repeated header values across requests in the same connection. Typical reduction is 80-90% on header byte volume.
  • Stream priorities. A way to express that some requests matter more than others. Like push, deployment was problematic; the priority semantics were under-specified, browsers and servers disagreed, and most implementations either ignore priorities or use a simple weighted round-robin. RFC 9218 (Extensible Prioritization) is the current attempt at fixing this.

The wire-level effect: in a packet capture, an HTTP/2 connection looks like a sequence of binary frames. You can't cat the bytes and read them like HTTP/1.1; you need a parser. Wireshark dissects HTTP/2 well. curl -v --http2 shows the negotiation; curl -v --http2 --trace-ascii /tmp/trace shows full frame details.

Why HTTP/2 still inherits TCP head-of-line blocking

Multiplexing solves application-layer HOL: requests aren't queued behind each other in HTTP semantics. But HTTP/2 still rides on a single TCP connection, and TCP delivers a single in-order byte stream.

If a TCP segment is lost in flight, TCP buffers all subsequent bytes until the loss is recovered. From HTTP/2's perspective, this means every stream is blocked behind the lost bytes — even streams whose data wasn't in the lost segment. The transport doesn't know about streams; it knows about bytes.

So HTTP/2 over TCP has:

  • No application HOL within HTTP semantics.
  • Strong transport HOL at the TCP layer.

In practice, this means HTTP/2 performs worse than parallel HTTP/1.1 connections under packet loss. With six independent TCP connections, a loss on one connection blocks only the requests on that connection — the other five proceed. With one HTTP/2 connection, a single loss blocks all hundred multiplexed streams.

Empirical studies of HTTP/2 vs HTTP/1.1 with parallel connections showed HTTP/2 was a clear win on low-loss paths and a clear loss on high-loss paths. The crossover was around 1-2% packet loss. That's the engineering pressure that motivated HTTP/3.

QUIC changes the substrate

QUIC (RFC 9000, 2021) is a new transport protocol designed at Google starting in 2012, standardized by the IETF, now ubiquitous as the substrate for HTTP/3. The headline feature relevant here: independent stream loss recovery. QUIC streams are first-class transport objects. Each stream has its own sequence space, its own ordering, its own retransmission. A packet lost on stream A doesn't block stream B.

That single change, applied to HTTP, eliminates transport HOL blocking across multiplexed requests.

QUIC packs several other features that matter:

  • Encrypted by default. QUIC integrates TLS 1.3. There is no unencrypted QUIC; the protocol's headers themselves are partially encrypted to prevent middlebox protocol-fixation.
  • Faster connection establishment. A QUIC + TLS handshake is 1-RTT in the cold case, 0-RTT for resumed connections. TCP + TLS 1.3 is 2-RTT cold, 1-RTT resumed. The savings are real on long-RTT paths.
  • Connection migration. A QUIC connection is identified by a connection ID, not the 4-tuple (src IP, src port, dst IP, dst port). If the client changes networks (Wi-Fi to cellular), the connection survives the IP change. TCP can't do this — a TCP connection is bound to its 4-tuple.
  • Built on UDP. Specifically because TCP's behavior is implemented in kernels everywhere and would take decades to evolve. UDP is a thin demultiplexing layer the QUIC user-space library can put almost anything on top of. (See Module 1.6 for the UDP design rationale.)

The trade-off: QUIC moves transport logic from the kernel to user space (the QUIC library inside the application). Performance is slightly worse than TCP at the same congestion-control settings because user-space transports don't have the same syscall efficiency. But the protocol-evolution agility — being able to deploy a fix in an application update rather than waiting for kernel deployment everywhere — has been worth the per-packet overhead.

HTTP/3 mapping over QUIC

HTTP/3 is HTTP semantics carried over QUIC streams. Each request gets its own QUIC stream; the request and response flow as bytes on that stream; control information uses dedicated QUIC streams.

The result:

  • No transport HOL. A loss on stream 5 doesn't affect stream 7.
  • Same multiplexing benefits as HTTP/2 at the application semantics layer.
  • Faster connection setup thanks to QUIC's integrated handshake.
  • Connection migration when the client moves networks.

The HTTP/3 wire format inside QUIC streams is similar to HTTP/2's frame structure: HEADERS, DATA, etc. But the framing is simpler because QUIC handles ordering and reliability — HTTP/3 doesn't need to worry about stream identifiers (the QUIC stream ID does that work) or framing across packet boundaries.

The migration path on the open internet has been gradual:

  • Server announces HTTP/3. Either via the Alt-Svc header (Alt-Svc: h3=":443"), via DNS HTTPS records (the alpn=h3 parameter), or both.
  • Client tries HTTP/3. On a fresh connection, the client makes a UDP/QUIC connection to port 443 (or whatever the announcement specified).
  • Fall back if QUIC fails. Some networks block UDP, drop QUIC packets, or have NATs that don't handle long-lived UDP flows well. If QUIC fails, the client falls back to HTTP/2 over TCP/TLS.

By 2026, most major sites support HTTP/3, and most modern browsers prefer it. Cloudflare, Google, Meta, Akamai, and Apple all serve significant fractions of their public traffic over HTTP/3.

HPACK vs QPACK

Header compression sounds like a minor detail. It isn't, because of how it interacts with multiplexing and HOL.

HPACK (RFC 7541) is HTTP/2's compression scheme. It uses two tables: a static table of common header names/values (e.g., :method=GET) and a dynamic table that grows as the connection progresses. To send a header, the encoder either references an existing table entry by index or sends a new value plus an instruction to add it to the dynamic table.

The compression is excellent — repeated headers across requests cost a single byte. But HPACK's dynamic table has a critical property: the encoder and decoder must update their tables in lockstep. If the decoder receives a reference to a table entry it hasn't seen the insertion for yet, decoding fails for the whole stream.

In HTTP/2 over TCP, this works because TCP guarantees in-order delivery. The decoder always sees insertions before the references that depend on them.

In HTTP/3 over QUIC, where streams are independent and may arrive out of order, this lockstep property breaks. If stream 5 references a dynamic-table entry inserted on stream 3, and stream 3 hasn't arrived yet, stream 5 has to block waiting. That re-introduces transport HOL via header compression — the very problem QUIC was supposed to solve.

QPACK (RFC 9204) is HTTP/3's redesigned header compression. The key change: dynamic-table updates flow on a separate dedicated control stream. Request streams reference table entries with version numbers indicating which dynamic-table state they assume. If a request arrives that references an entry not yet inserted, the decoder buffers that request only — other streams aren't blocked.

QPACK is more complex than HPACK but preserves the no-transport-HOL property of QUIC at the application layer. This is the compression equivalent of QUIC's stream-aware reliability: handle the cross-stream dependencies explicitly so a single stream's pause doesn't block all of them.

Operational consequences

A few things that have changed in real deployments:

Connection establishment cost. HTTP/1.1 over TLS: 3 RTT to first byte (TCP handshake + TLS 1.2). HTTP/2 over TLS 1.3: 2 RTT to first byte. HTTP/3 over QUIC: 1 RTT to first byte cold, 0 RTT resumed. On long-RTT paths, the savings dominate page-load time.

Alt-Svc and HTTPS records. Servers announce protocol upgrades via HTTP headers (Alt-Svc) or DNS records (HTTPS RR with alpn=). The Alt-Svc header is a per-response upgrade hint; HTTPS records put the same information in DNS, eliminating the need for an initial HTTP/1.1 round trip to learn about HTTP/3 support.

Middlebox traversal. TCP traffic passes through stateful firewalls, IDS, and inspection middleboxes that have been built up over decades of HTTP/1.1 use. QUIC traffic — UDP with encrypted headers — looks unfamiliar to many middleboxes. Some networks drop or rate-limit it. The fallback to HTTP/2 over TCP keeps things working but loses the QUIC benefits.

Debugging requires different tooling. tcpdump/Wireshark dissect HTTP/2 if you provide TLS keys. HTTP/3's QUIC encryption is harder to inspect; you need both TLS keys and QUIC's connection state. Modern Wireshark handles it; older versions don't. curl --http3 -v is the simplest interactive debugging tool.

Server software. Adoption of HTTP/3 in server software is uneven. nginx added HTTP/3 in 1.25 (2023). Apache's mod_http3 is experimental. Caddy supports HTTP/3 by default. CDNs (Cloudflare, Akamai, Fastly) all support it; origin servers behind CDNs often don't need to.

"HTTP/3 is faster" is too vague. On low-RTT, low-loss paths, HTTP/2 and HTTP/3 perform similarly; HTTP/3's connection migration and 0-RTT may not show measurable benefit in lab tests. On high-RTT paths or paths with non-zero loss, HTTP/3 wins more clearly. On networks that drop or rate-limit UDP, HTTP/3 falls back to HTTP/2 and gains nothing. The benefit is conditional; the version negotiation matters.

Hands-on exercise

Exercise 1 — Inspect negotiated HTTP versions

# Force HTTP/1.1
curl -I --http1.1 https://example.com

# Force HTTP/2
curl -I --http2 https://example.com

# Try HTTP/3 (requires curl built with HTTP/3 support; check `curl --version`)
curl -I --http3 https://cloudflare.com

Each command negotiates the requested version via TLS ALPN. If the server supports it, you get a response in that version. If not, HTTP/2 falls back to HTTP/1.1 and HTTP/3 falls back to HTTP/2.

To see the ALPN negotiation, add -v:

curl -v --http2 https://example.com 2>&1 | grep -E "ALPN|HTTP/"

You'll see the client offering protocols (h2, http/1.1) and the server selecting one.

Exercise 2 — Reason about head-of-line blocking

Suppose three multiplexed requests are in flight on one HTTP/2 connection: stream 1 (an HTML page), stream 3 (a CSS file), stream 5 (a JS file). All three are sending data in parallel from the server.

A single TCP segment in the middle of the response stream is lost.

Which streams pause until TCP recovers the loss?

All three. TCP can't deliver any further bytes from any stream until the lost segment is retransmitted, because the byte stream must remain in order at the kernel level. HTTP/2 is multiplexed at the application layer, but the underlying TCP byte stream is single-threaded.

What if the same setup runs over HTTP/3 / QUIC?

Only the stream whose data was in the lost packet. QUIC's per-stream sequence numbers let it deliver bytes from the other streams as soon as they arrive. The lost-segment stream pauses; the others continue.

This is the textbook example of why HTTP/3 wins on lossy paths and HTTP/2 doesn't.

Common misconceptions

"HTTP/2 fixed head-of-line blocking completely." It fixed application-layer HOL: requests aren't queued behind each other in HTTP semantics. Transport-layer HOL persists because HTTP/2 rides on a single in-order TCP byte stream. Any segment loss in TCP blocks every multiplexed stream until recovery.

"HTTP/3 is just HTTP over UDP." QUIC provides reliability, encryption, ordering per stream, congestion control, and connection identification. UDP is the packet substrate; QUIC is the actual transport. HTTP/3 inherits all of QUIC's properties, of which "uses UDP" is the least interesting.

"Header compression is just about saving bytes." Compression also interacts with stream ordering. HPACK's dynamic-table requirement (decoder must see insertions in order) re-introduces HOL blocking when applied across QUIC streams. QPACK was designed to break that dependency at the cost of more protocol complexity.

"More streams means more parallelism for free." Streams share the connection's flow control budget. Each stream has its own per-stream window, but the connection has an aggregate window too. Server scheduling, TCP cwnd, and the receiver's ability to consume data still cap practical concurrency.

"HTTP/1.1 is obsolete." It's everywhere. Inside CDNs (origin connections often still HTTP/1.1), in debugging tools, in low-resource environments, in fallback paths. The text framing remains the easiest way to teach HTTP, the easiest to debug by hand, and the most universally supported. Treating HTTP/1.1 as legacy misses how much of the network actually uses it.

Further reading

  1. RFC 9110 — HTTP Semantics. The version-independent contract — methods, status codes, headers. Read once for the conceptual baseline.
  2. RFC 9112 — HTTP/1.1. The text-framing version mapping. Replaces the old RFC 7230.
  3. RFC 9113 — HTTP/2. The binary-framing version mapping. Replaces the original RFC 7540.
  4. RFC 7541 — HPACK. HTTP/2's header compression.
  5. RFC 9000 — QUIC. The transport that made HTTP/3 possible.
  6. RFC 9114 — HTTP/3. The version mapping over QUIC.
  7. RFC 9204 — QPACK. HTTP/3's redesigned header compression.
  8. RFC 9460 — Service Binding (SVCB / HTTPS records). The DNS mechanism for advertising HTTP/3 endpoints without an HTTP/1.1 round trip.

The next module — TLS 1.3 handshake byte by byte — covers what happens between TCP setup and HTTP semantics: the cryptographic handshake that turns a plaintext TCP connection into a confidential, authenticated channel.