# HTTP/2 and HTTP/3: what changed and why it matters

> Learn how HTTP/2 and HTTP/3 changed connections, multiplexing, compression, QUIC, deployment, and the web performance decisions you make today.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-14 | Updated: 2026-08-09 | Topics: [Networking](https://flaviocopes.com/tags/network/) | Canonical: https://flaviocopes.com/http2-http3/

[HTTP](https://flaviocopes.com/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.

```mermaid
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](https://www.rfc-editor.org/rfc/rfc9110.html). HTTP/2 is defined by [RFC 9113](https://www.rfc-editor.org/rfc/rfc9113.html). HTTP/3 is defined by [RFC 9114](https://www.rfc-editor.org/rfc/rfc9114.html).

Changing the protocol version does not change this application code:

```js
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](https://flaviocopes.com/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.

```mermaid
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:

```text
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](https://flaviocopes.com/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:

```http
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:

```mermaid
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](https://www.rfc-editor.org/rfc/rfc7541.html).

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](https://www.rfc-editor.org/rfc/rfc9218.html) 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](https://developer.chrome.com/blog/removing-push/) 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](https://www.rfc-editor.org/rfc/rfc9000.html).

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.

```mermaid
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](https://www.rfc-editor.org/rfc/rfc9204.html). 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:

```http
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.

```mermaid
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](https://flaviocopes.com/tools/cache-control/) 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 `Connection` do 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 `statusText` containing 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
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.1`
- `h2`
- `h3`

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:

```bash
curl --version
```

Look for `HTTP2` in the Features line.

Then request only the response headers:

```bash
curl --http2 -I https://flaviocopes.com/
```

The first line identifies the selected HTTP version:

```text
HTTP/2 200
```

You can print the version without all the headers:

```bash
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:

```bash
curl --http3 https://flaviocopes.com/ -o /dev/null
```

Use `--http3-only` when you want the test to fail instead of falling back:

```bash
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:

```bash
curl -I https://flaviocopes.com/
```

Look for:

```http
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:

1. The CDN or server has HTTP/3 enabled.
2. UDP port 443 can reach the edge.
3. The certificate and HTTPS configuration are valid.
4. The response advertises HTTP/3 or DNS discovery is configured.
5. The browser has not cached an old alternative-service result.
6. 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:

1. Serve the entire site over HTTPS.
2. Enable HTTP/2 at the CDN, reverse proxy, or server.
3. Enable HTTP/3 where your edge supports it.
4. Keep HTTP/2 and HTTP/1.1 fallback available.
5. Remove domain sharding added only for connection slots.
6. Revisit giant bundles and sprite sheets.
7. Keep sensible bundling, compression, and cache boundaries.
8. Make important resources discoverable early.
9. Verify `h2` and `h3` in DevTools.
10. 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.
