System Design
HTTP/1.1 vs HTTP/2 vs HTTP/3
The protocol of the web has been quietly reinvented twice. Each version solved a real bottleneck the last one couldn't. Here's what changed across HTTP/1.1, HTTP/2, and HTTP/3 - and why it matters for performance.
HTTP is the protocol of the web - the language browsers and servers use to exchange requests and responses. It feels like a fixed, unchanging thing, but it's actually been reinvented twice in pursuit of speed. HTTP/1.1 served the web for decades, HTTP/2 overhauled how data moves over a connection, and HTTP/3 changed the foundation underneath entirely. Each version solved a real bottleneck the previous one couldn't, and understanding the progression explains a lot about web performance. This guide walks through all three: what each does, what problem it solved, and how they compare.
HTTP/1.1: one thing at a time
HTTP/1.1 was the workhorse of the web for a very long time, and it's beautifully simple: it's a text-based protocol where a client sends a request and the server sends back a response, over a TCP connection. Human-readable, easy to debug, straightforward.
Its core limitation is that, on a single connection, requests are handled essentially one at a time. A browser sends a request and largely waits for the response before the next can proceed on that connection. This causes head-of-line blocking at the application level: if one response is slow, the requests behind it wait. Since a modern web page needs dozens or hundreds of resources (images, scripts, stylesheets), this serialization is a real bottleneck.
The workaround browsers used was to open multiple parallel connections to a server (typically around six) to fetch resources concurrently. This helped, but opening many connections has its own overhead, and six is a small number against hundreds of resources. HTTP/1.1 was showing its age as web pages grew heavier - it simply wasn't built for the modern web's volume of small requests.
HTTP/2: many things at once
HTTP/2 was designed to fix HTTP/1.1's concurrency problem, and its central innovation is multiplexing. Over a single connection, HTTP/2 can handle many requests and responses at the same time, interleaved. Instead of one-at-a-time (or a handful of parallel connections), one HTTP/2 connection carries many concurrent streams of data. This eliminates the application-level head-of-line blocking of HTTP/1.1 and removes the need to open many connections.
To enable this, HTTP/2 made several changes:
- Binary, not text. HTTP/2 is a binary protocol (framed into binary messages), which is more efficient to parse and is what enables the multiplexing of streams. (Less human-readable, but tooling handles that.)
- Multiplexed streams. Many independent request/response streams share one connection, interleaved, so a slow response doesn't block others (at the application layer).
- Header compression. HTTP requests carry repetitive headers; HTTP/2 compresses them, reducing overhead, especially valuable when making many requests.
- Server push (a feature allowing servers to proactively send resources the client will likely need - though this turned out to be tricky in practice and saw limited use).
HTTP/2 dramatically improved performance for the modern web by letting one connection do the work that previously took many, without the one-at-a-time bottleneck. But it had a subtle remaining flaw, rooted in the layer beneath it.
graph TD
subgraph HTTP1["HTTP/1.1"]
A1[Request 1] --> A2[wait] --> A3[Request 2] --> A4[wait]
end
subgraph HTTP2["HTTP/2: multiplexed over one connection"]
B[One connection] --> B1[Stream 1]
B --> B2[Stream 2]
B --> B3[Stream 3]
end
This diagram contrasts HTTP/1.1 and HTTP/2. In HTTP/1.1, requests on a connection happen largely one at a time - request one, then wait, then request two, then wait - causing serialization. In HTTP/2, a single connection is multiplexed into many concurrent streams - stream one, two, and three all flowing at once over the same connection - so many requests and responses proceed in parallel without waiting on each other.
HTTP/2's remaining problem: TCP head-of-line blocking
HTTP/2 multiplexed many streams over one connection - but that connection still ran over TCP. And TCP guarantees in-order delivery of all its data. So if a single packet is lost, TCP holds up everything behind it until that packet is retransmitted - including all the other multiplexed streams, even though they're logically independent.
This is TCP head-of-line blocking, and it's subtle: HTTP/2 solved head-of-line blocking at the application layer (streams don't block each other in HTTP/2's logic), but the transport layer (TCP) reintroduced it, because TCP doesn't know the streams are independent - it just sees one ordered byte stream, and one lost packet stalls all of it. On a lossy network, this undercuts HTTP/2's multiplexing benefit. To truly fix it, you'd have to change the transport itself - which is exactly what HTTP/3 did.
HTTP/3: a new foundation
HTTP/3 makes a radical change: it abandons TCP as its transport and runs over QUIC, a new transport protocol built on top of UDP. This sounds drastic, but it's the only way to eliminate TCP's head-of-line blocking - by not using TCP.
QUIC re-implements the reliability and ordering that applications need, but does so per-stream and with awareness of the independent streams, so a lost packet in one stream doesn't block the others. This finally delivers on HTTP/2's multiplexing promise even on lossy networks: independent streams are genuinely independent, all the way down. Building on UDP (which is connectionless and has no built-in ordering) gave QUIC the freedom to design exactly the behavior it wanted, rather than being constrained by TCP's strict global ordering.
HTTP/3 / QUIC brought other improvements too:
- Faster connection setup. QUIC combines the transport handshake and the encryption (TLS) handshake into one, so establishing a secure connection takes fewer round-trips than the traditional TCP-then-TLS sequence. Connections start faster.
- Connection migration. QUIC connections can survive a change in network (like a phone switching from Wi-Fi to cellular) without breaking, because the connection isn't tied to the IP address the way TCP connections are. This is valuable for mobile.
- Encryption built in. QUIC encrypts by default (TLS is integral), so HTTP/3 is always secure.
HTTP/3 is the current cutting edge, eliminating the last head-of-line blocking, speeding up connection setup, and improving mobile experience - by rebuilding the transport from UDP up.
The three compared
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Format | Text | Binary | Binary |
| Concurrency | One at a time per connection | Multiplexed streams | Multiplexed streams |
| Transport | TCP | TCP | QUIC (over UDP) |
| Head-of-line blocking | Yes (application level) | At TCP level only | Eliminated |
| Connection setup | TCP + TLS handshakes | TCP + TLS handshakes | Combined, faster |
| Header compression | No | Yes | Yes |
| Key advance | The baseline | Multiplexing over one connection | New transport, no HoL blocking |
Why HTTP/1.1 lasted - and the hacks it forced
HTTP/1.1 served the web for around fifteen years, and understanding why it lasted (and what pain it caused) makes the later versions' improvements concrete. The protocol was simple and worked, but as pages grew from a few resources to hundreds, its one-request-at-a-time limit forced developers into a set of workarounds - optimizations that existed purely to route around HTTP/1.1's constraints:
- Domain sharding. Since browsers limited connections per domain (around six), developers served resources from multiple domains (
img1.example.com,img2.example.com) to trick the browser into opening more parallel connections. - Spriting and concatenation. To reduce the number of requests, developers combined many small images into one "sprite sheet," and concatenated many JavaScript or CSS files into one. Fewer files meant fewer requests to serialize.
- Inlining. Small resources were embedded directly into the HTML to avoid separate requests entirely.
These were clever, but they were hacks - extra build and architecture complexity, all to compensate for the protocol's serial nature. When HTTP/2 arrived with multiplexing, most of these workarounds became unnecessary (and some even counterproductive). That's a good way to appreciate what HTTP/2 changed: a whole genre of web-performance tricks existed solely because HTTP/1.1 couldn't do many requests at once, and HTTP/2 made that genre largely obsolete.
Adoption, fallback, and compatibility
A practical point: these versions coexist, and clients and servers negotiate which to use. When a browser connects, it and the server agree on the highest version both support, falling back gracefully - a server supporting HTTP/2 or HTTP/3 uses it with capable clients and falls back to HTTP/1.1 for older ones. So adopting a newer version doesn't break older clients; it's an upgrade for those who can use it.
This is why you can enable HTTP/2 or HTTP/3 without leaving anyone behind - the negotiation handles it. HTTP/2 is now very widely supported; HTTP/3 is newer with growing adoption across major browsers, CDNs, and large services. In practice, much of the benefit arrives "for free" when you use a modern CDN or web server that supports the newer protocols - the CDN often handles HTTP/2 and HTTP/3 to the user while talking to your origin over whatever it supports. So a lot of real-world adoption happens at the CDN/edge layer without application changes.
A closer look at QUIC
Since HTTP/3's whole advantage comes from QUIC, it's worth understanding why building on UDP was the move. TCP is implemented deep in operating systems and is hard to change - its behavior (including the strict global ordering that causes head-of-line blocking) is baked in everywhere. By building QUIC on top of UDP (which is minimal and just delivers datagrams), QUIC's designers got a blank slate: they could implement reliability, ordering, and congestion control in QUIC itself, exactly how they wanted, rather than inheriting TCP's constraints. This is why QUIC can make streams genuinely independent (no cross-stream blocking) - it built ordering per-stream rather than globally.
It also let QUIC integrate encryption directly and combine the connection and security handshakes, cutting setup round-trips. And because a QUIC connection is identified by a connection ID rather than being tied to the IP-and-port the way TCP is, it can survive a network change (Wi-Fi to cellular) without re-establishing - connection migration. None of this would have been possible while staying on TCP. So "HTTP/3 runs on UDP" isn't a quirk; it's the enabling decision that made every HTTP/3 improvement achievable.
What this means for you
For a developer or architect, the practical implications of the HTTP evolution:
- You usually get the benefits by using modern infrastructure. Enabling HTTP/2 and HTTP/3 is typically a configuration on your web server or, more commonly, automatic when you put a modern CDN in front of your site. You rarely change application code to benefit.
- Old HTTP/1.1 optimizations can be dropped (or hurt). On HTTP/2, the sharding/spriting/concatenation hacks are no longer needed and can even reduce performance (HTTP/2 prefers many small cacheable files over one big concatenated blob). Knowing the protocol version informs your optimization strategy.
- It mostly "just works," but knowing the version helps debugging. Performance characteristics differ by version, so when diagnosing slowness, knowing whether you're on HTTP/1.1, /2, or /3 - and what each implies - helps you reason about where time is going.
The headline for practitioners: the protocol got dramatically better, you mostly get those gains by running modern servers and CDNs, and the old workarounds can be retired.
Where HTTP sits, and what it carries
It's worth placing HTTP in the stack to see what's changing and what isn't across versions. HTTP is an application-layer protocol - the language of requests and responses (GET this page, POST this data, here's the response). That semantic layer - the methods, the status codes, the headers, the idea of requests and responses - stays essentially the same across HTTP/1.1, /2, and /3. What changes between versions is how that conversation is transported: the framing (text vs binary), the concurrency (serial vs multiplexed), and the transport beneath (TCP vs QUIC).
This is a useful distinction because it means upgrading HTTP versions doesn't change how you write web applications - a GET request is a GET request in all three. Your APIs, your request methods, your status codes all work the same. What changes is the efficiency of delivering them. So the HTTP evolution is fundamentally about transport efficiency, not about changing the web's request-response model. The application semantics are stable; the plumbing underneath got faster. That separation - stable semantics, evolving transport - is exactly why these upgrades could happen without breaking the web, and why you benefit from them largely transparently. You keep speaking the same HTTP; it just travels better.
The throughline of the evolution
Step back and the progression tells a clear story, each version solving what the last couldn't:
- HTTP/1.1: simple but serial - one request at a time per connection, so pages with many resources were slow.
- HTTP/2: multiplexing over one connection solved application-level serialization, but TCP underneath reintroduced head-of-line blocking on packet loss.
- HTTP/3: a new transport (QUIC over UDP) eliminated TCP's head-of-line blocking entirely, plus faster setup and connection migration.
Each step removed the previous version's bottleneck, and the through-line is the relentless pursuit of getting many resources delivered concurrently and quickly. The deepest lesson is that solving HTTP/2's remaining problem required changing the layer beneath HTTP - you can't fix transport-level blocking at the application level, so HTTP/3 went all the way down to the transport.
Interview: HTTP versions come up in system design and web-performance discussions. The strong answer is the evolution and why of each: HTTP/1.1 was serial (one request at a time per connection); HTTP/2 added multiplexing (many streams over one connection) but still suffered TCP head-of-line blocking; HTTP/3 moved to QUIC over UDP to eliminate that and speed up connection setup. Knowing that HTTP/3's key insight was changing the transport (because the problem lived below HTTP) shows real depth. Mentioning multiplexing, head-of-line blocking, and the TCP-to-QUIC shift signals you understand web performance at the protocol level.
The bottom line
HTTP has evolved through three versions, each solving the last one's bottleneck. HTTP/1.1 is simple and text-based but handles requests largely one at a time per connection, serializing the many resources a modern page needs. HTTP/2 introduced multiplexing - many concurrent streams over a single binary connection, plus header compression - eliminating application-level serialization, but left TCP head-of-line blocking (one lost packet stalls all streams). HTTP/3 rebuilt the transport on QUIC (over UDP), eliminating head-of-line blocking entirely, speeding up connection setup by combining handshakes, and enabling connection migration for mobile.
The evolution is a steady push to deliver many resources concurrently and fast, culminating in HTTP/3's insight that the remaining bottleneck lived in the transport layer beneath HTTP. Understand the progression, and web performance at the protocol level makes sense. We cover web protocols and performance across System Design Fundamentals.
Go deeper
- MDN - HTTP - the web's protocol documented in depth.
- HTTP/2 (Wikipedia) - multiplexing and the binary protocol.
- HTTP/3 (Wikipedia) - QUIC and the move off TCP.
- The System Design Primer - HTTP in a system design context.