HTTP Evolution: 1.1 to 2 to 3

HTTP is the protocol your applications actually speak, and it has quietly reinvented itself twice to fight one persistent enemy: head-of-line blocking, where one slow thing stalls everything behind it. The journey from HTTP/1.1 to HTTP/2 to HTTP/3 is the story of chasing that problem down the stack — and understanding it explains why modern connections are so much faster.

The lower layers deliver bytes securely; HTTP is what those bytes mean — the request/response protocol of the web and most APIs. But “HTTP” isn’t one thing: it has three major versions in active use, each solving a performance problem the last couldn’t. This post traces that evolution — HTTP/1.1’s limits, HTTP/2’s multiplexing, HTTP/3’s move to QUIC — around the recurring villain of head-of-line blocking, so you understand what version your systems use and why it matters.

HTTP, the constant

Across all versions, the semantics of HTTP are stable: a client sends a request (a method like GET/POST, a URL, headers, an optional body), and the server returns a response (a status code, headers, a body). Methods, status codes, and headers work the same whether you’re on HTTP/1.1 or HTTP/3 — what changes between versions is the transport mechanics: how requests and responses are actually moved over the connection, and how efficiently. So the evolution isn’t about what HTTP says but how fast and concurrently it can say it. Keep that split in mind: semantics constant, transport improving.

HTTP/1.1 and its bottleneck

HTTP/1.1 was the web’s workhorse for two decades. Its model: a request is sent on a TCP connection, and the response comes back — but crucially, on a single connection, one request/response must complete before the next can be sent. This creates head-of-line (HOL) blocking at the HTTP layer: if one response is slow, every request queued behind it on that connection waits, even if they could have been answered instantly.

HTTP/1.1 on one connection:
  Request A ──▶ (slow response A) ──────────▶
                                  Request B waits... ──▶ response B
  → B is blocked by A even though B was ready

The workarounds of the HTTP/1.1 era reveal the pain:

These hacks worked but were symptoms of the underlying problem: HTTP/1.1 couldn’t efficiently multiplex many requests over one connection. That’s what HTTP/2 set out to fix.

HTTP/2: multiplexing over one connection

HTTP/2 solved HTTP/1.1’s blocking by introducing multiplexing: many requests and responses can be in flight simultaneously over a single TCP connection, interleaved as independent streams. No more waiting for one response before sending the next; no more opening six connections.

HTTP/2 on ONE connection:
  Request A ──▶  ┐
  Request B ──▶  ├─ all in flight at once, responses interleaved
  Request C ──▶  ┘
  → A being slow no longer blocks B and C at the HTTP layer

Its improvements:

HTTP/2 made the HTTP/1.1 workarounds largely unnecessary — bundling and multiple connections became counterproductive — and sped up the web substantially. But it had a subtle remaining flaw, one it couldn’t fix because of the layer it ran on.

The TCP head-of-line blocking problem

HTTP/2 removed HOL blocking at the HTTP layer — but a deeper one remained at the TCP layer. Recall that TCP guarantees ordered delivery (the TCP post): it delivers bytes to the application strictly in order, holding back later bytes until earlier lost ones are retransmitted. HTTP/2 runs all its multiplexed streams over one TCP connection, so:

HTTP/2 streams A, B, C multiplexed over ONE TCP connection.
A packet for stream A is LOST →
  TCP holds ALL subsequent bytes (including B's and C's) until A's packet is retransmitted
  → streams B and C stall waiting for A's lost packet — TCP-level HOL blocking

Because TCP can’t tell that the bytes belong to independent streams, a single lost packet stalls all the multiplexed streams until retransmission — the blocking HTTP/2 fixed at its own layer reappears at the transport layer beneath it. This is worse on lossy networks (mobile, congested links). HTTP/2 fundamentally can’t fix this, because the problem lives in TCP, below HTTP. Solving it required changing the transport itself.

HTTP/3: HTTP over QUIC

HTTP/3 eliminates TCP head-of-line blocking by abandoning TCP entirely and running over QUIC — a modern transport built on UDP (the TCP/UDP post foreshadowed this). QUIC reimplements the reliability and ordering TCP provided, but with independent streams, so a lost packet only stalls its own stream:

HTTP/3 over QUIC (on UDP):
  Streams A, B, C are independent within QUIC.
  A packet for stream A is lost →
  only stream A waits; B and C proceed unaffected → no cross-stream HOL blocking

Why build on UDP? Because TCP’s ordered-delivery-across-everything is exactly the constraint causing the problem, and TCP is deeply baked into operating systems and network hardware (hard to change). By building on UDP (which imposes no ordering) and implementing its own per-stream reliability, QUIC gets TCP’s guarantees where wanted without its cross-stream blocking. QUIC’s other wins:

HTTP/3 is the current state of the art, increasingly deployed, and it completes the arc: HTTP/1.1’s per-request blocking → HTTP/2’s multiplexing (HTTP-layer fix) → HTTP/3’s QUIC (transport-layer fix). Each step chased head-of-line blocking one layer deeper.

What this means for backend engineers

You don’t usually implement these protocols, but the evolution shapes real decisions:

HTTP is the language your applications speak, and its three versions are a masterclass in chasing one performance problem down the stack. With HTTP understood, the last building blocks are the infrastructure that sits in front of your servers — load balancers and proxies — the next post.

Key takeaways

Further reading

Sources & References

HTTP/2, HTTP/3, and QUIC