HTTP/2 and HTTP/3: what changed and why it matters
By Flavio Copes
Learn how HTTP/2 and HTTP/3 changed connections, multiplexing, compression, QUIC, deployment, and the web performance decisions you make today.
HTTP still looks familiar.
A browser sends a request. A server returns a response. We still use methods such as GET and POST, status codes such as 200 and 404, headers, URLs, and response bodies.
But the way those messages travel changed a lot.
HTTP/1.1 sends messages as text over TCP. HTTP/2 turns them into binary frames and multiplexes many exchanges over one TCP connection. HTTP/3 carries the same HTTP semantics over QUIC, a transport built on UDP.
That last paragraph contains most of the story. It does not explain why the changes matter.
In this tutorial, I want to build the full mental model. We will start with the problem, follow one page load through all three versions, and finish with the choices you should make on a modern site.
flowchart LR
A["HTTP semantics<br/>methods, URLs, headers, status"] --> B["HTTP/1.1<br/>text over TCP"]
A --> C["HTTP/2<br/>frames and streams over TCP"]
A --> D["HTTP/3<br/>frames and streams over QUIC"]
The short version
HTTP/2 fixed the request queue inside HTTP. HTTP/3 fixed a remaining queue inside the transport.
Here is the practical difference:
| Version | Message transport | Parallel requests | Header compression | Packet loss |
|---|---|---|---|---|
| HTTP/1.1 | text over TCP | no native multiplexing | no | affects its TCP connection |
| HTTP/2 | binary frames over TCP | multiplexed streams | HPACK | can stall every stream on the connection |
| HTTP/3 | frames over QUIC/UDP | multiplexed QUIC streams | QPACK | normally stalls only the affected stream |
All three versions can exist at the same time. HTTP/3 did not switch off HTTP/2, and HTTP/2 did not erase HTTP/1.1.
The browser and server negotiate a version they both support. If HTTP/3 cannot connect, the browser can fall back to HTTP/2 or HTTP/1.1.
What stayed the same
HTTP has two layers that are easy to mix up:
- Semantics define what a request or response means.
- Mapping and transport define how that message crosses the network.
The current semantics live in RFC 9110. HTTP/2 is defined by RFC 9113. HTTP/3 is defined by RFC 9114.
Changing the protocol version does not change this application code:
const response = await fetch('/api/profile')
if (response.ok) {
const profile = await response.json()
}
The request still has a method and headers. The response still has a status and a body.
HTTP caching also keeps working. Cache-Control, ETag, conditional requests, and 304 Not Modified do not disappear when the connection uses HTTP/3.
The wire representation is different. Your application model is not.
The HTTP/1.1 problem
HTTP/1.1 does not have a multiplexing layer.
It can reuse a TCP connection for multiple requests. That is much better than opening a new connection for every file. But requests on one connection cannot make independent progress in the way HTTP/2 streams can.
HTTP/1.1 pipelining was designed to send several requests without waiting for each response. Responses still had to arrive in request order, and browsers found many unreliable intermediaries. Pipelining never became the general solution.
Browsers instead opened several TCP connections to the same origin. Six connections per origin became a common limit, although the exact number is an implementation detail.
Imagine a page that needs HTML, CSS, JavaScript, two fonts, and twenty images. The browser has far more work than available connection slots.
sequenceDiagram
participant B as Browser
participant C1 as TCP connection 1
participant C2 as TCP connection 2
B->>C1: GET /app.css
B->>C2: GET /app.js
C1-->>B: app.css
B->>C1: GET /font.woff2
C2-->>B: app.js
B->>C2: GET /hero.webp
C1-->>B: font.woff2
C2-->>B: hero.webp
This creates three costs.
First, requests wait for an available connection. This is application-level head-of-line blocking.
Second, every connection has setup and congestion-control work. More connections do not make the network free.
Third, each connection learns about available network capacity separately. Several competing connections can use the path less efficiently.
The workarounds we built
Developers changed sites to work around those limits.
We combined icons into sprite sheets. We concatenated CSS and JavaScript into large bundles. We inlined images as data URLs. We split resources across domains such as static1.flaviocopes.com and static2.flaviocopes.com to get more browser connections.
These techniques reduced waiting under HTTP/1.1. They also had costs.
A change to one line could invalidate a large bundle. A sprite could force the browser to download icons it never used. Domain sharding added DNS, connection, and TLS setup. Inlining removed the browser’s ability to cache a resource independently.
HTTP/2 was designed to remove the connection-slot problem instead of adding more workarounds around it.
How HTTP/2 starts
HTTP/2 was first standardized in 2015. The current specification is RFC 9113, published in 2022.
On a public HTTPS site, the browser normally chooses HTTP/2 during the TLS handshake. It uses ALPN, Application-Layer Protocol Negotiation, and the protocol identifier h2.
Conceptually, the client offers a list:
h2
http/1.1
The server selects one it supports.
This negotiation happens before the browser sends the HTTP request. The URL does not become http2://..., and application routes do not change.
HTTP/2 can run without TLS using a mode commonly called h2c. Browsers normally use HTTP/2 over TLS for public websites. In practice, HTTPS is the baseline.
HTTP/2 is compatible with HTTP semantics, not with the HTTP/1.1 wire format. An HTTP/1.1 client cannot parse HTTP/2 frames. Negotiation and fallback make the upgrade transparent to most applications.
Binary framing
HTTP/1.1 represents requests using text lines:
GET /avatar.jpg HTTP/1.1
Host: flaviocopes.com
Accept: image/avif,image/webp,*/*
HTTP/2 does not send that text block over the connection.
It splits messages into frames. A HEADERS frame carries a compressed field section. One or more DATA frames carry the body. Other frames manage settings, flow control, cancellation, and the connection.
Each frame contains a stream identifier. That identifier tells the receiver which request or response the frame belongs to.
The binary format is not mainly about saving a few characters. It gives both endpoints an unambiguous structure they can parse and interleave.
Streams and multiplexing
An HTTP/2 connection contains many streams. Each request and response exchange uses its own stream.
Frames from different streams can share the same TCP connection:
flowchart LR
B["Browser"] --> C["One TCP connection"]
C --> S1["Stream 1<br/>HTML"]
C --> S3["Stream 3<br/>CSS"]
C --> S5["Stream 5<br/>JavaScript"]
C --> S7["Stream 7<br/>image"]
The server might send part of the CSS response, then part of the image, then more CSS. The browser rebuilds each message from its stream’s frames.
A slow application response no longer forces every response behind it to wait. The other streams can keep moving.
This is the core HTTP/2 improvement.
It also improves connection reuse. One warm TLS connection can carry many requests without the browser opening a small fleet of TCP connections.
HPACK header compression
HTTP headers repeat a lot of data.
Every request to one origin might include the same user agent, accepted encodings, cookies, and other fields. Sending the full text each time wastes bytes, especially when the response itself is small.
HTTP/2 uses HPACK, defined by RFC 7541.
HPACK combines a static table of common fields with a dynamic table shared by the encoder and decoder. Later requests can refer to table entries instead of repeating every value.
This is compression for HTTP fields. It does not replace Brotli or gzip compression for CSS, JavaScript, HTML, JSON, and other response bodies.
Flow control
Multiplexing creates a new question: how much data can each sender transmit?
HTTP/2 uses flow-control windows for individual streams and for the whole connection. A receiver sends WINDOW_UPDATE frames to advertise more capacity.
This prevents one sender from overwhelming the other endpoint. It also stops one large response from consuming unlimited buffers.
Flow control is hop by hop. A CDN can have one HTTP/2 connection to a browser and a different connection to the origin. Each hop manages its own windows.
Prioritization is more complicated than the original story
The first HTTP/2 specification included a detailed priority tree. Clients could describe dependencies and weights between streams.
That design was difficult to implement consistently. Many deployments ignored the signals. RFC 9113 deprecated the original priority signaling scheme.
The newer Extensible Prioritization Scheme uses simpler urgency and incremental hints. Support still varies across clients, servers, and intermediaries.
The useful lesson is not “the server always sends the important file first.”
The useful lesson is that resource discovery still matters. Put critical CSS where the browser can find it. Do not hide the main image behind late JavaScript. Use preload and fetchpriority only when measurement shows they help.
What happened to HTTP/2 server push
HTTP/2 also introduced server push. A server could send a resource before the browser requested it.
The idea looked great. The server knows the page needs app.css, so why wait?
The hard part was knowing what the browser already had cached and which resources it truly needed. An unnecessary push consumes bandwidth and can compete with the HTML.
Chrome disabled HTTP/2 server push by default in version 106 after deployments showed little use and no clear net performance gain. Chrome’s explanation recommends preload and 103 Early Hints as more practical alternatives.
Do not build a new optimization plan around server push.
The problem HTTP/2 could not fix
HTTP/2 multiplexes streams inside one connection. That connection still uses TCP.
TCP gives the application one reliable, ordered byte stream. If packet 20 goes missing but packet 21 arrives, TCP cannot deliver the later bytes to the HTTP/2 layer yet. It waits for packet 20 to be retransmitted.
The missing packet might belong to one image. TCP does not know about HTTP/2 streams. Every stream shares the same ordered TCP byte stream, so unrelated work can pause too.
This is transport-level head-of-line blocking.
On a fast, clean network, recovery can be quick. On mobile networks, congested Wi-Fi, or long-distance connections, the pause can be more visible.
HTTP/3 changes the transport to solve this.
HTTP/3 runs over QUIC
HTTP/3 was standardized in 2022. It maps HTTP semantics onto QUIC, defined by RFC 9000.
QUIC runs over UDP, but calling it “unreliable HTTP over UDP” is wrong.
QUIC implements reliability, congestion control, flow control, streams, and connection management itself. It uses UDP as the substrate because that lets QUIC evolve those features without waiting for operating-system TCP stacks to change.
QUIC version 1 uses TLS 1.3 as part of its handshake. HTTP/3 is encrypted. There is no public cleartext HTTP/3 mode equivalent to ordinary HTTP over TCP.
flowchart TD
H["HTTP semantics"] --> H2["HTTP/2 framing"]
H2 --> TLS2["TLS"]
TLS2 --> TCP["TCP"]
H --> H3["HTTP/3 framing"]
H3 --> Q["QUIC streams + TLS 1.3"]
Q --> UDP["UDP"]
The diagram is conceptual. With HTTPS over TCP, TLS sits above TCP. QUIC integrates the TLS handshake into its transport protocol.
Independent QUIC streams
QUIC exposes multiple reliable streams inside one connection.
Bytes remain ordered within a stream. Different streams do not need to arrive in order relative to each other.
If a packet carrying image data is lost, that image stream waits for recovery. A CSS stream whose packets arrived can continue.
This removes the connection-wide head-of-line blocking HTTP/2 inherits from TCP.
Packet loss can still reduce shared bandwidth. Congestion affects the path, and a missing packet still delays its own stream. HTTP/3 does not repeal network physics.
QPACK replaces HPACK
HTTP/3 compresses headers too, but it cannot directly reuse HPACK.
HPACK assumes field sections arrive in order across the connection. QUIC deliberately removes that total ordering between streams.
HTTP/3 uses QPACK, defined by RFC 9204. It separates updates to the dynamic table from the request streams that refer to it.
You do not configure QPACK in application code. The important part is understanding why HTTP/3 needed a new header-compression design.
Faster connection setup
A new HTTPS connection over TCP usually needs a TCP handshake followed by a TLS handshake before application data can flow.
QUIC combines transport and cryptographic setup. A new connection can normally start in one round trip. A client returning to a server may use 0-RTT data and send some application data immediately.
0-RTT is not free speed for every request.
Early data can be replayed by an attacker. Servers must only accept it for operations safe against replay. A GET for a public page is very different from a POST that charges a card.
Your CDN or server handles this policy. Do not assume every repeat visit skips all setup.
Connection migration
A TCP connection is identified by source and destination IP addresses and ports. Move a phone from Wi-Fi to cellular, and that identity changes.
QUIC uses connection IDs that are not tied to one network path. The connection can survive an address change after validating the new path.
This is useful for mobile devices. A download or API session does not necessarily restart just because the network changed.
Migration is a protocol capability, not a promise that every application transition will be invisible. The network can still disappear, time out, or block UDP.
How a browser discovers HTTP/3
The first visit often starts with a TCP-based connection. The server can advertise HTTP/3 using an Alt-Svc response header:
Alt-Svc: h3=":443"; ma=86400
This tells the browser that an equivalent service is available using HTTP/3 on UDP port 443. ma=86400 says the advertisement can be remembered for 86,400 seconds.
The browser can try QUIC on a later request or connection. Modern clients can also discover alternative services through DNS HTTPS records.
If UDP is blocked or the QUIC attempt fails, RFC 9114 tells clients to try a TCP-based HTTP version. This fallback matters on corporate networks, hotel Wi-Fi, and older network equipment.
sequenceDiagram
participant B as Browser
participant E as CDN edge
B->>E: HTTPS request over TCP
E-->>B: HTTP/2 response + Alt-Svc: h3
B->>E: Try QUIC over UDP 443
alt QUIC works
E-->>B: HTTP/3 response
else QUIC is blocked
B->>E: Continue with HTTP/2
end
HTTP/3 is not automatically faster
HTTP/3 has structural advantages, especially on networks with packet loss or changing paths. That does not mean every request completes faster.
On a stable connection close to the server, HTTP/2 may perform just as well. QUIC encryption and packet processing can use more CPU. A poor implementation can erase protocol advantages. The first HTTP/3 attempt can also race against a working TCP connection.
Measure the complete user experience.
Protocol choice does not fix a slow database, an overloaded server, an uncacheable response, a two-megabyte JavaScript bundle, or an image that is ten times larger than it needs to be.
What changes for frontend work
Some HTTP/1.1 advice is obsolete. Some was overcorrected after HTTP/2 arrived.
Stop domain sharding
Putting assets on several hostnames can prevent connection reuse. Each hostname may need DNS lookup, connection setup, TLS negotiation, and its own congestion state.
Keep first-party resources on as few origins as practical. A separate image CDN can still make sense when it provides resizing, caching, and global delivery. Do not create extra domains only to unlock more HTTP/1.1 connection slots.
Do not bundle only to reduce request count
HTTP/2 and HTTP/3 handle concurrent requests well. That removes the old rule that every file must be concatenated.
It does not mean “ship one file per function.” Every request still has headers, scheduling, cache, parsing, and execution costs.
Split code around routes and features. Let users download what they need. Keep stable dependencies cacheable across deployments. Then measure the result.
Do not use sprites only to save connections
Independent SVG or image files can be cached and updated separately. Modern formats and image CDNs often matter more than removing one request.
Sprites still make sense for some graphics workflows. The old six-connection limit is no longer a good reason by itself.
Keep optimizing discovery
Multiplexing helps after the browser knows a URL exists.
If JavaScript loads, runs, and only then reveals the main image, the protocol cannot request that image earlier. HTML structure, preload scanning, preload, modulepreload, and fetchpriority still affect the request start time.
Keep caching
A fast request is slower than no request.
Hashed static assets can use long cache lifetimes. HTML usually needs a shorter policy. APIs and private pages need deliberate rules. The Cache-Control builder can help you reason about browser and shared-CDN caching separately.
What changes for backend and API work
Most application handlers should stay protocol-independent.
The server framework receives an HTTP request and returns an HTTP response. A reverse proxy or CDN often terminates HTTP/2 or HTTP/3 before the request reaches your process.
There are still a few details to remember:
- Connection-specific headers such as
Connectiondo not belong in HTTP/2 or HTTP/3 messages. - HTTP/1.1 chunked transfer encoding is not used in HTTP/2 or HTTP/3. Framing already marks the data.
- Reason phrases are not part of HTTP/2 or HTTP/3, so do not depend on
statusTextcontaining text. - Streaming still works, but proxies and buffering policies can change when bytes reach the client.
- One client can create many concurrent streams, so servers still need request limits and backpressure.
Do not branch business logic on h2 versus h3. Treat the negotiated protocol as an operational and performance detail unless you are building network infrastructure.
How I use HTTP/2 and HTTP/3
This site is a static Astro build on Cloudflare Pages.
I do not run a QUIC server inside Astro. Cloudflare terminates the browser connection at its edge. The page can reach the visitor over HTTP/3 while Cloudflare handles deployment, certificates, negotiation, and fallback.
The current response makes that visible:
HTTP/2 200
server: cloudflare
alt-svc: h3=":443"; ma=86400
My command-line curl negotiated HTTP/2. The Alt-Svc header advertises HTTP/3 to clients that support QUIC.
The same separation applies to my Cloudflare Pages Functions. The /purchase webhook and course-access endpoint still read ordinary requests and return ordinary responses. They do not need separate HTTP/2 and HTTP/3 implementations.
I use the protocols as infrastructure, then focus application work on the things I control:
- generating static HTML where possible
- keeping first-party assets on reusable origins
- serving downloads through Cloudflare R2
- setting correct cache rules
- reducing image and JavaScript bytes
- keeping server work fast
- checking the protocol from the browser and command line
For a Hono or Astro application with several parallel API requests, multiplexing helps prevent those requests from fighting for a small HTTP/1.1 connection pool. I still avoid unnecessary requests. A better transport does not make a chatty API design free.
For large downloads, HTTP/3 can recover better when one stream loses packets or the user changes networks. File size, edge location, cache status, and server throughput can matter more. I look at all of them.
When I would not manage HTTP/3 myself
If a site already sits behind Cloudflare, Fastly, Akamai, or another capable CDN, I would let the edge handle HTTP/3.
Running QUIC directly means operating UDP listeners, certificates, updates, observability, resource limits, and fallbacks. That is justified for a CDN, proxy, browser, or specialized high-traffic service. It is rarely the best use of time for a normal content site or CRUD application.
I would also not choose HTTP/3 as a substitute for WebSockets, Server-Sent Events, or WebTransport. HTTP/3 improves HTTP request and response delivery. It does not turn every application into a bidirectional realtime protocol.
How to inspect the negotiated protocol
The easiest place is browser DevTools.
Open the Network panel and enable the Protocol column. Depending on the browser, you will see values such as:
http/1.1h2h3
Reload the page. Check the HTML and several first-party resources.
Do not be surprised if the first navigation uses HTTP/2 and later requests use HTTP/3. Discovery, cached Alt-Svc information, connection racing, and existing connections can affect the result.
Inspect HTTP/2 with curl
First, check how your curl build was compiled:
curl --version
Look for HTTP2 in the Features line.
Then request only the response headers:
curl --http2 -I https://flaviocopes.com/
The first line identifies the selected HTTP version:
HTTP/2 200
You can print the version without all the headers:
curl -sS -o /dev/null \
-w 'HTTP version: %{http_version}\n' \
https://flaviocopes.com/
Inspect HTTP/3 with curl
Your curl build needs HTTP/3 support. Many system builds have HTTP/2 but not HTTP/3.
If curl --version lists HTTP/3, try:
curl --http3 https://flaviocopes.com/ -o /dev/null
Use --http3-only when you want the test to fail instead of falling back:
curl --http3-only -I https://flaviocopes.com/
The difference is useful during diagnosis. A successful normal request does not prove QUIC worked if the client silently used HTTP/2.
Read the headers carefully
This command shows the HTTP/3 advertisement even when the current request uses HTTP/2:
curl -I https://flaviocopes.com/
Look for:
Alt-Svc: h3=":443"; ma=86400
Alt-Svc advertises availability. It does not prove the current response used HTTP/3.
That distinction catches a common debugging mistake.
Common reasons HTTP/3 does not appear
If the browser stays on HTTP/2, check these areas:
- The CDN or server has HTTP/3 enabled.
- UDP port 443 can reach the edge.
- The certificate and HTTPS configuration are valid.
- The response advertises HTTP/3 or DNS discovery is configured.
- The browser has not cached an old alternative-service result.
- A VPN, firewall, proxy, or antivirus tool is not blocking QUIC.
Fallback is expected behavior. HTTP/2 is not an error state.
How to measure whether it helped
Do not compare protocol names and declare victory.
Test under conditions that expose the difference:
- a warm repeat visit and a cold first visit
- low and high network latency
- clean and lossy connections
- desktop and mobile networks
- cached and uncached resources
- one large response and many parallel responses
Look at request start times, stalled time, time to first byte, and completion time. Then connect those network numbers to user-facing metrics such as LCP and INP.
Run several samples. Connection reuse and network variation can make one trace misleading.
Common mistakes
“HTTP/2 makes every request parallel”
Streams are multiplexed, but servers, browsers, flow-control windows, prioritization, and application limits still decide how work progresses.
“HTTP/3 has no head-of-line blocking”
It removes blocking between independent QUIC streams. Bytes inside one stream remain ordered, and congestion can affect the entire connection.
“UDP means packets can disappear”
Raw UDP does not guarantee delivery. QUIC builds reliable streams, recovery, congestion control, and security on top of it.
“HTTP/3 removes the need for a CDN”
The protocol improves transport. A CDN shortens distance, caches content, absorbs traffic, and terminates connections close to users.
“More files are always better now”
Request count is cheaper than it was under HTTP/1.1. It is not free. Split around useful caching and loading boundaries.
“The backend must be rewritten for QUIC”
Usually the CDN or reverse proxy handles it. Your application can keep returning normal HTTP responses.
A migration checklist
If you are modernizing an older site, work through this list:
- Serve the entire site over HTTPS.
- Enable HTTP/2 at the CDN, reverse proxy, or server.
- Enable HTTP/3 where your edge supports it.
- Keep HTTP/2 and HTTP/1.1 fallback available.
- Remove domain sharding added only for connection slots.
- Revisit giant bundles and sprite sheets.
- Keep sensible bundling, compression, and cache boundaries.
- Make important resources discoverable early.
- Verify
h2andh3in DevTools. - Test on constrained networks, not only local broadband.
The model to remember
HTTP/1.1 has no native request multiplexing. Browsers used several TCP connections to create parallelism.
HTTP/2 adds binary frames and independent HTTP streams over one TCP connection. HPACK compresses repeated headers. TCP packet loss can still pause every stream on that connection.
HTTP/3 maps HTTP onto QUIC. QUIC provides encrypted, independent transport streams over UDP, faster setup opportunities, and connection migration. QPACK adapts header compression to those independently delivered streams.
For most web developers, the upgrade belongs at the CDN or server edge. Your job is to use HTTPS, remove obsolete HTTP/1.1 workarounds, keep caching and resource discovery strong, and verify the result under real network conditions.
That is what changed. And that is why it matters.
Related posts about network: