Quick Answer

HTTP/2 replaced HTTP/1.1's one-request-per-connection-slot model with a binary protocol that multiplexes many requests over a single TCP connection, removing the need for old tricks like domain sharding. HTTP/3 goes further by replacing TCP itself with QUIC, a transport built on UDP, so a single lost packet no longer stalls every other in-flight request on the connection the way it still can under HTTP/2. Both changes are about eliminating artificial waiting, not compressing pages harder.

What HTTP/1.1 Actually Got Wrong

HTTP/1.1 is a plain-text protocol, and by default it can only have one request in flight per TCP connection at a time — you send a request, you wait for the full response, then you can send the next one on that same connection. Pipelining (sending several requests without waiting) technically existed in the spec, but browsers essentially never enabled it by default, because a slow response at the front of the queue still blocked every response behind it from being delivered, even if they had already arrived. That problem — one slow thing blocking unrelated things behind it — is called head-of-line blocking, and it shows up more than once in this story.

Browsers worked around the one-request-at-a-time limit the obvious way: opening more TCP connections. Most browsers settled on a cap around six simultaneous connections per hostname. That number is not arbitrary politeness — it directly shaped how the entire industry built websites for over a decade.

A whole set of front-end performance folklore exists specifically to route around that six-connection ceiling: domain sharding (splitting static assets across fake subdomains like static1.example.com and static2.example.com purely so the browser would treat them as separate hosts and open more parallel connections), image spriting, and aggressively concatenating every JS and CSS file into one giant bundle to minimize the number of separate requests needed. None of these were really solving a bandwidth problem — they were working around a connection-count limit.

HTTP/2 was designed specifically to remove the need for these workarounds by fixing the actual constraint, instead of giving developers more elaborate ways to route around it.

What HTTP/2 Actually Changed: One Connection, Many Streams

HTTP/2 replaced the plain-text request/response model with a binary framing layer, and it allows many requests and responses — called streams — to be interleaved over a single TCP connection at the same time. Instead of opening six connections to work around a one-at-a-time limit, a browser opens one connection and multiplexes everything through it.

It also introduced HPACK, a header-compression scheme built specifically around the fact that HTTP headers repeat enormously across requests on the same connection — the same cookies, the same user-agent string, the same accept headers, sent again on every single request under HTTP/1.1. HPACK lets both sides maintain a shared table of previously-seen header fields and send small references instead of the same raw bytes over and over.

This isn't just spec-reading — it's directly checkable. Node.js ships a built-in HTTP/2 client, and running this against a real, live Cloudflare-fronted site confirms the negotiation actually happens:

const http2 = require('http2');
const client = http2.connect('https://www.cloudflare.com');

const req = client.request({ ':path': '/' });
req.on('response', (headers) => {
  console.log('status:', headers[':status']);
});
req.on('end', () => {
  console.log('ALPN protocol negotiated:', client.alpnProtocol);
  client.close();
});
req.resume();
req.end();

Running that script produces this actual, captured output — the client and server agreed on HTTP/2 (h2) during the TLS handshake's ALPN negotiation step:

status: 200
ALPN protocol negotiated: h2

Multiplexing is just as directly observable. Firing five concurrent requests over one HTTP/2 connection and logging each response as it lands produces this real output:

stream 1  /                -> 200  (+305ms)
stream 3  /robots.txt      -> 200  (+66ms)
stream 5  /favicon.ico     -> 200  (+64ms)
stream 7  /humans.txt      -> 404  (+330ms)
stream 9  /security.txt    -> 404  (+330ms)
All 5 requests completed over ONE TCP connection, socket reused: true

Two things are worth noticing in that output. First, client-initiated stream IDs are always odd numbers (1, 3, 5, 7, 9) by design — even numbers are reserved for server-initiated streams, used by the now largely-abandoned Server Push feature. Second, the completion order has nothing to do with request order: /robots.txt and /favicon.ico finished in roughly 65ms while the root path took 305ms, and all five streams shared the exact same TCP connection the entire time. That out-of-order completion is multiplexing actually working, not something broken.

Server Push itself — letting a server proactively send a response the client didn't explicitly ask for yet, anticipating it would be needed — was part of the original HTTP/2 spec but proved hard to use correctly in practice (over-pushing resources the client already had cached wasted real bandwidth). Chrome disabled it by default starting with Chrome 106 in September 2022, and it has effectively disappeared from real-world use since.

The Problem HTTP/2 Could Not Fix: Head-of-Line Blocking Inside TCP

HTTP/2 solved head-of-line blocking at the application layer — one slow HTTP response no longer blocks unrelated ones on the same connection. But every one of those multiplexed streams still travels over a single, ordered TCP byte stream underneath, and TCP's entire job is guaranteeing in-order delivery of that one stream.

Here is the sharp edge that creates: if a single packet belonging to stream A gets lost on the network, TCP will not hand any later-arriving bytes up to the application — including bytes belonging to a completely unrelated stream B that arrived at the network card perfectly fine — until the lost packet is retransmitted and finally arrives. From the application's point of view, every multiplexed stream on that connection stalls, waiting on one lost packet that may have nothing to do with most of them.

This matters more in practice than it might look on paper, because packet loss is not rare on the networks a huge share of real traffic travels over — congested WiFi, cellular handoffs between towers, lossy last-mile links. On a clean, low-loss wired connection this problem rarely surfaces. On a phone on patchy 4G, it can turn a single dropped packet into a stall affecting every in-flight request on the page.

This exact failure mode — TCP's ordering guarantee turning one lost packet into a stall for unrelated data — is the direct motivation for HTTP/3.

What HTTP/3 Actually Changed: Swapping Out TCP for QUIC

HTTP/3 keeps HTTP's request/response semantics but replaces the transport underneath entirely. Instead of running over TCP, it runs over QUIC, a transport protocol built on top of UDP. Because QUIC implements its own stream concept directly, each HTTP/3 stream gets independent ordering and independent loss recovery — a lost packet belonging to stream A now only stalls stream A. Every other stream on the same connection keeps flowing while that one packet gets retransmitted.

QUIC also folds the transport handshake and the TLS 1.3 handshake into a single combined step, rather than TCP's handshake completing first and then a separate TLS handshake layered on top of it — cutting a full round trip off connection setup compared to TCP+TLS. And because QUIC identifies a connection by a connection ID rather than the traditional four-tuple of source/destination IP and port, a connection can survive a network change — a phone moving from WiFi to cellular, for instance — without tearing down and renegotiating from scratch, something that reliably broke a TCP connection outright.

The standards themselves are recent and worth being precise about: HTTP/3 is standardized as RFC 9114, published in June 2022, layered on top of QUIC's core specification, RFC 9000, published in May 2021. HTTP/2's original specification, RFC 7540 from 2015, was itself later obsoleted by an updated RFC 9113 in 2022 — the protocol most people still call "HTTP/2" has quietly had its formal spec revised since it first shipped.

How to Check What a Server — and Your Own Tools — Actually Support

A server can advertise HTTP/3 support to a client that's currently talking to it over HTTP/1.1 or HTTP/2, using the Alt-Svc response header, so that a future connection attempt can try QUIC directly instead of guessing. Here is a real header captured from Cloudflare:

alt-svc: h3=":443"; ma=86400

And from Google:

Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

The ma value is the max-age of that advertisement in seconds (86400 seconds is 24 hours; 2592000 seconds is 30 days) — essentially "you can trust that HTTP/3 is available here for this long without checking again." Google's header also advertises h3-29, an older, pre-final draft version of the QUIC transport that some servers keep advertising for older clients that never updated to the finalized RFC 9114 version.

Worth a genuinely practical aside from testing this directly while researching this piece: attempting to check HTTP/2 or HTTP/3 support with curl on the machine used here failed outright —

$ curl --http2 https://www.cloudflare.com
curl: option --http2: the installed libcurl version does not support this

— because that particular curl build (8.17.0) simply was not compiled with HTTP/2 or HTTP/3 support linked in at all; its protocol list only listed http and https, nothing else. Node's built-in http2 module worked without any issue because it ships its own implementation independent of the system's curl/libcurl. The practical lesson: newer protocol support is not automatic just because a tool is otherwise up to date — it depends on how that specific binary was built. Run curl -V and check its "Features"/protocol list, or just look for an alt-svc line in a plain response header dump, before assuming your own tooling is testing what you think it's testing.

What This Means for the Code You Actually Write

For almost every team, HTTP/2 and HTTP/3 support is a server, reverse-proxy, or CDN configuration setting, not an application code change — enabling it in Nginx, Caddy, or a CDN dashboard is typically the entire task, since both protocols still deliver the same HTTP requests and responses your application code already expects.

What does need reconsidering is the HTTP/1.1-era folklore mentioned earlier. Domain sharding actively hurts performance under HTTP/2 and HTTP/3, because it forces the browser back into opening multiple separate connections — each with its own slow-start ramp-up and its own handshake — instead of letting one connection multiplex everything efficiently. Aggressively bundling every script into one giant file also loses some of its old justification: many small, independently cacheable files, multiplexed in parallel over one connection, can now compete favorably with one enormous bundle that has to be re-downloaded in full the moment any single line inside it changes.

As of when this was written, HTTP/3 is supported by every major modern browser (Chrome, Firefox, Safari 16+, Edge) with automatic fallback to HTTP/2 or HTTP/1.1 when a server or network doesn't support it — including networks that specifically block UDP traffic, which QUIC depends on, and which some corporate and mobile networks do throttle or block, making that fallback path a real necessity rather than a formality. Whether your specific hosting stack or CDN has HTTP/3 turned on by default is worth checking directly in its documentation rather than assuming.

Frequently Asked Questions

Do I need to change my application code to support HTTP/2 or HTTP/3? No, in almost all cases. Both protocols are transport-level changes handled by your web server, reverse proxy, or CDN — your application still receives ordinary HTTP requests and sends ordinary responses.
Can I tell which protocol a page actually loaded over? Yes — open your browser's DevTools, go to the Network tab, and add the "Protocol" column (right-click any column header to enable it). It will show h2, h3, or http/1.1 per request.
Is HTTP/2 encrypted the same way HTTP/1.1 is? The HTTP/2 spec technically allows unencrypted connections (h2c), but in practice every major browser only supports HTTP/2 over TLS. HTTP/3 goes further and mandates TLS 1.3 as part of the QUIC transport itself — there is no unencrypted HTTP/3.
Why would a browser fall back to HTTP/1.1 on a modern site? Usually a network-level block on UDP traffic, which QUIC (and therefore HTTP/3) requires — some corporate firewalls and mobile carrier networks restrict or throttle UDP. Browsers detect this and fall back to HTTP/2 or HTTP/1.1 automatically.
Does HTTP/3 replace TLS? No — QUIC includes TLS 1.3 as part of its handshake rather than layering it on top separately the way TCP+TLS does, but the encryption itself is still standard TLS 1.3, not a replacement for it.