6 min read

Q03 — How did HTTP evolve from 1.1 → HTTP/2 → HTTP/3 — what problem does each version fix?


What is HTTP doing at the most basic level?

HTTP is a protocol for fetching resources — documents, images, JSON, video — by sending a request and getting a response. Every version of HTTP does this. What changes between versions is how efficiently it does it, driven by the real-world bottlenecks each era exposed.

To understand why each version exists, you need to understand what was slow about the previous one.


Why was HTTP/1.0 too slow to use for modern web pages?

In HTTP/1.0, every request required its own TCP connection. You open a connection, send a request, get a response, close the connection. Repeat for every resource.

A modern web page loads dozens to hundreds of resources: HTML, CSS files, JavaScript files, images, fonts. In HTTP/1.0, each one required a full TCP handshake (one round trip) before the request could even be sent. On a connection with 100ms round-trip latency, fetching 50 resources sequentially means 5 full seconds spent just on handshakes, before any data is transferred.

This was obviously intolerable. HTTP/1.1 fixed it.


What did HTTP/1.1 fix — and what new problem did it create?

HTTP/1.1 introduced keep-alive (persistent connections). After a response is received, the TCP connection stays open. The next request reuses the same connection, skipping the handshake.

This was a massive improvement. But it exposed the next bottleneck: HTTP/1.1 is still sequential within a connection. Request 2 cannot be sent until the response to request 1 is fully received. This is called head-of-line blocking at the HTTP layer.

Browsers worked around this by opening multiple parallel TCP connections to the same server — typically 6 connections per domain. Each connection can have one request in flight. So you get 6 requests in parallel, but 6 TCP connections is a lot of overhead (6 handshakes, 6 congestion control states, more memory on both sides).

HTTP/1.1 also has a structural inefficiency: headers. Every request sends HTTP headers as plain text, and many headers are identical across requests — Host, User-Agent, Accept, Cookie. On a page with 80 requests, you're sending the same 800 bytes of headers 80 times, uncompressed.

By the early 2010s, pages had grown to hundreds of resources and HTTP/1.1's limitations were measurably slowing down the web. Google built SPDY as a proof of concept for what came next.


What did HTTP/2 fix?

HTTP/2 kept TCP as the transport but redesigned everything above it.

Multiplexing: HTTP/2 introduces the concept of streams — multiple logical request/response pairs can be in flight simultaneously on a single TCP connection. Request 2 doesn't wait for response 1. Request 50 doesn't wait for responses 1 through 49. Multiple streams interleave their data frames on the same connection. This eliminates HTTP-level head-of-line blocking and kills the need for multiple parallel TCP connections.

Header compression (HPACK): HTTP/2 maintains a shared compression table on both sides. The first request sends the full headers; subsequent requests send only the diff — headers that changed. Sending the same Cookie or User-Agent in the 50th request costs almost nothing because it's already in the table. Header overhead drops dramatically.

Binary framing: HTTP/1.1 sends text — "GET /index.html HTTP/1.1\r\nHost: example.com\r\n...". HTTP/2 encodes everything as binary frames. Binary is more compact, faster to parse, and less error-prone than text parsing.

Server push: The server can proactively send resources the client hasn't requested yet. If the server knows the client will need style.css after receiving index.html, it can push it immediately rather than waiting for the client to parse the HTML and ask.

HTTP/2 was a significant leap. But it exposed the next bottleneck — and this one was deeper.


What problem did HTTP/2 not solve?

HTTP/2 fixed head-of-line blocking at the HTTP layer. But it still runs on TCP, and TCP has its own head-of-line blocking.

Here's the problem: TCP delivers a byte stream in order. HTTP/2 multiplexes multiple streams onto one TCP connection, but TCP doesn't know about HTTP/2 streams. If a TCP packet carrying stream 3's data is lost, TCP stalls all delivery — streams 1, 2, 4, 5, 6 are all frozen waiting for stream 3's lost packet to be retransmitted.

HTTP/2 on a network with 2% packet loss can actually perform worse than HTTP/1.1 with multiple connections, because HTTP/1.1's 6 connections are independent — a lost packet only stalls one of them.

The root problem: TCP's head-of-line blocking is in the kernel, below the HTTP layer. You can't fix it from HTTP/2. The only way to fix it is to replace TCP.


What did HTTP/3 do — and why did it need to reinvent the wheel?

HTTP/3 replaced TCP with QUIC — a new transport protocol built on top of UDP.

QUIC reimplements the parts of TCP that are good (reliability, ordering, flow control, congestion control) but at the stream level, not the connection level. Within QUIC, streams are independent. If a packet carrying stream 3's data is lost, only stream 3 is stalled — streams 1, 2, 4, 5, 6 keep flowing. Head-of-line blocking is eliminated at the transport layer.

Why build on UDP rather than fix TCP? Because TCP is implemented in operating system kernels, and kernels update slowly. Deploying a new TCP variant would take a decade of OS updates to reach most devices. UDP is already universal. QUIC runs in user space — it can be updated as fast as you update an app or a browser.

Connection establishment: TCP requires a handshake (1 round trip) before TLS requires its own handshake (1–2 more round trips). QUIC combines connection setup and TLS into a single 1-round-trip exchange. On a repeat visit to a known server, QUIC can do 0-RTT — send data in the very first packet, with no handshake at all.

Connection migration: A TCP connection is tied to the client's IP address and port. If you're on WiFi and switch to cellular, your IP changes — the TCP connection breaks and you have to re-establish. QUIC uses connection IDs instead of IP addresses to identify connections. When your IP changes, QUIC migrates the connection transparently. Your video call doesn't drop when you walk out of range of WiFi.


Why didn't we build HTTP/3 sooner?

QUIC's design was only possible because:

  1. TLS 1.3 simplified the crypto handshake enough to fold into QUIC's connection setup
  2. Most networks now allow UDP traffic on port 443 (older firewalls blocked UDP)
  3. The engineering maturity existed to build a reliable transport in user space

The web hit HTTP/1.1's limits around 2012, HTTP/2 shipped in 2015, HTTP/3 was standardized in 2022. Each version was a response to bottlenecks that only became visible at the scale and page complexity of the prior era.


The mental model

code
HTTP/1.0:  one request per TCP connection → terrible for pages with many resources
              ↓ Fixed by:
HTTP/1.1:  persistent connections, but sequential within each → HTTP head-of-line blocking
           Workaround: 6 parallel TCP connections per domain
           New problem: uncompressed headers, text parsing overhead
              ↓ Fixed by:
HTTP/2:    multiplexed streams on one connection, header compression, binary framing
           Remaining problem: TCP head-of-line blocking when packets are lost
              ↓ Fixed by:
HTTP/3:    QUIC over UDP — per-stream reliability, 0-RTT, connection migration
           No kernel dependency, updatable like an app

Each version didn't appear because engineers got bored. Each version appeared because the previous one became the bottleneck at the scale and complexity of the real web.