All lessons
Web basicsIntermediate9 min readIn my stack

HTTP/1.1, HTTP/2 and HTTP/3

Multiplexing, header compression, QUIC, and what they mean for speed.

  • HTTP/1.1
  • HTTP/2
  • HTTP/3
  • QUIC
  • Multiplexing
Step 1/4
HTTP/1.1: one connection carries one request at a time. Files wait in line like cars on a one-lane road.

In 30 seconds

HTTP's meaning has not changed in decades, but the way messages travel has. HTTP/1.1 sends one request at a time per connection, HTTP/2 sends many in parallel over one TCP connection, and HTTP/3 does the same over QUIC on UDP to avoid stalls when packets are lost. Enabling the newer versions is usually a few lines of config and makes pages load faster, especially on mobile.

Why I use it

  • I enable HTTP/2 and HTTP/3 on the sites I run because it is cheap speed for every visitor.
  • Knowing how each version works tells me which old performance tricks to drop, such as domain sharding and huge bundles.
  • Mobile users on unstable networks benefit the most from HTTP/3, and many of my products are used from phones.
  • It helps me reason about real-time features and long-lived connections behind nginx and a CDN.

How it works

01HTTP/1.1: one request at a time

HTTP/1.1 (1997, refined in RFC 9112) is text-based. Keep-alive lets a connection be reused, but on each connection requests are answered strictly one after another: if one response is slow, everything behind it waits. This is head-of-line blocking at the HTTP level. Browsers work around it by opening about six connections per host, and developers used tricks: domain sharding across several hostnames, image sprites, and concatenating all JavaScript into one file. Headers are sent as plain text with every request, so cookies and user agents repeat thousands of times. Analogy: a single checkout lane where each customer must finish before the next starts, so shops open six lanes.

02HTTP/2: multiplexing and HPACK

HTTP/2 (2015, now RFC 9113) keeps the same methods, headers and status codes but changes the wire format to binary frames. Its key feature is multiplexing: many requests and responses travel at the same time over one TCP connection as independent streams, interleaved frame by frame. One slow response no longer blocks the others at the HTTP level. HPACK compresses headers using a table of previously sent values, so repeated cookies and headers cost a few bytes. Prioritization lets the browser signal what matters most. Server push existed but was removed from browsers because it rarely helped; 103 Early Hints with preload replaces it. In practice browsers only use HTTP/2 over TLS, negotiated through ALPN during the handshake.

  • One connection per origin instead of six: fewer handshakes.
  • Domain sharding now hurts: it forces extra connections.
  • Many small files are fine; split bundles by route instead of one giant file.

03The remaining problem: TCP head-of-line blocking

HTTP/2 fixed blocking at the HTTP layer but not underneath. TCP guarantees that bytes arrive in order. If one packet is lost, TCP holds back everything after it until the lost packet is retransmitted, even data that belongs to other streams. On a clean office network this is rare. On mobile networks with 1–2% packet loss, one lost packet stalls every image, script and API call on that connection. In bad conditions HTTP/2 over one connection can even be slower than HTTP/1.1 with six. TCP also lives in the operating system kernel, so improving it takes years of OS updates. This is why a new transport was needed.

04HTTP/3: QUIC over UDP

HTTP/3 (RFC 9114, 2022) runs over QUIC (RFC 9000), a transport built on UDP. QUIC implements reliability, congestion control and streams itself, and every stream is independent: a lost packet delays only its own stream. TLS 1.3 is built into QUIC, so a new connection needs one round trip instead of the two or three of TCP plus TLS, and resumed connections can send data immediately (0-RTT, which should be limited to safe, idempotent requests because it can be replayed). Connections are identified by a connection ID, not by IP and port, so a phone moving from Wi-Fi to mobile data keeps its connection. Headers use QPACK, a variant of HPACK. Browsers discover HTTP/3 through the Alt-Svc header or the HTTPS DNS record and fall back to HTTP/2 if UDP 443 is blocked.

05Enabling it in nginx and Cloudflare

On nginx 1.25.1 or newer, HTTP/2 is turned on with the http2 directive (the old listen ... http2 parameter is deprecated) and HTTP/3 with a quic listener on the same port, built with the QUIC module, which current official packages include. Advertise HTTP/3 with an Alt-Svc header and open UDP 443 in the firewall. On Cloudflare, HTTP/2 and HTTP/3 to visitors are on by default; you can check them under Speed or Network settings. The connection from Cloudflare to your origin is separate: it uses HTTP/1.1 or HTTP/2 over TCP, so the origin does not need HTTP/3 when it is behind Cloudflare. Verify with curl or the Protocol column in the browser's Network tab.

nginx
server {
    listen 443 ssl;
    listen 443 quic reuseport;   # reuseport only once per port
    http2 on;
    http3 on;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
# sudo ufw allow 443/udp
# curl -sI --http2 https://example.com/ | head -1

06What it means for performance work

With HTTP/2 or HTTP/3 enabled, focus on what still costs time. Round trips still matter, so keep the critical path short: preload the hero image and main font, avoid long redirect chains, and serve from a CDN near users. Bytes still matter, so compress and ship less JavaScript. Concatenation is less important, but thousands of tiny modules still have overhead; bundlers' route-level splitting is the sweet spot. Avoid domain sharding and unnecessary third-party origins, since each new origin needs DNS, connection and TLS setup; use preconnect for the ones you must keep. Measure real users: improvements from HTTP/3 show up mainly on slow and lossy networks, not on your fast desktop.

Use it when

  • HTTP/2: on every HTTPS site and API today; there is no real reason not to.
  • HTTP/3: public sites with many mobile users or users on distant, lossy networks.
  • Behind a CDN: let the CDN speak HTTP/3 to visitors and keep HTTP/2 or HTTP/1.1 to the origin.

Skip it when

  • Do not chase HTTP/3 on internal services in one data center; gains are tiny and debugging UDP is harder.
  • Corporate networks sometimes block UDP 443; HTTP/3 must always have an HTTP/2 fallback.

Alternatives

  • WebSockets

    Long-lived two-way channel; runs over HTTP/1.1 upgrade or HTTP/2 extended CONNECT.

  • gRPC

    Uses HTTP/2 streams for typed RPC between services.

  • WebTransport

    Newer API on HTTP/3 for low-latency streams and datagrams; still maturing.

Best practices

  1. Enable HTTP/2 everywhere and HTTP/3 on public edges, with TLS 1.3 on.
  2. Open UDP 443 in the firewall when enabling HTTP/3 on your own server.
  3. Stop domain sharding and image sprites; split bundles by route instead.
  4. Use preconnect for essential third-party origins and remove unnecessary ones.
  5. Use 103 Early Hints or preload instead of the removed server push.
  6. Verify the negotiated protocol in DevTools or curl after every config change.

Common mistakes

  • Enabling the quic listener but forgetting to open UDP 443, so browsers silently stay on HTTP/2.
  • Adding reuseport on several server blocks for the same port, which makes nginx fail to start.
  • Keeping HTTP/1.1-era tricks like sharding that now slow HTTP/2 down.
  • Allowing 0-RTT for requests that change data, exposing them to replay.

Deep dive

07Prioritization: from dependency trees to Extensible Priorities

Multiplexing raises a new question: when twenty responses share one connection, which bytes go first? HTTP/2 originally let clients build a dependency tree with weights, but it was complex, browsers used it inconsistently and many servers ignored it, so RFC 9113 deprecated it. The replacement is Extensible Priorities (RFC 9218), used by HTTP/2 and HTTP/3. A client sends a simple Priority header or frame: urgency u from 0 (highest) to 7, and an incremental flag i meaning the response is useful in pieces, like a progressive image. Servers and CDNs schedule data accordingly, so CSS and the LCP image can overtake a large analytics script. On the page, fetchpriority and preload are how you influence what the browser sends. If prioritization is broken somewhere along the path, multiplexing can actually delay critical files behind big ones.

http
GET /css/main.css HTTP/2
priority: u=0
 
GET /img/hero.avif HTTP/2
priority: u=1, i
 
GET /js/analytics.js HTTP/2
priority: u=5

08Flow control and HTTP/2-specific attacks

HTTP/2 and QUIC have flow control per stream and per connection: each side advertises a window of bytes it is willing to receive, and the sender must wait for WINDOW_UPDATE before sending more. Small default windows can limit throughput on fast, long links, which is why servers tune them. Multiplexing also created new denial-of-service patterns. In 2023 the Rapid Reset attack (CVE-2023-44487) opened and immediately cancelled streams in huge numbers, so servers did work for requests that were never counted against concurrency limits; it produced record-breaking floods. HPACK and QPACK need limits on header table size to prevent memory exhaustion. Defences are mostly in software updates and sane limits: keep nginx and proxies patched, cap concurrent streams (http2_max_concurrent_streams, default 128 in nginx), limit header sizes, and let a CDN absorb volumetric attacks.

09QUIC under the hood: handshakes, loss recovery, migration

QUIC packets are encrypted, including most of the header, so middleboxes cannot inspect or tamper with transport details, which keeps the protocol evolvable. Packet numbers never repeat, even for retransmissions, which removes TCP's ambiguity about which copy an acknowledgement refers to and makes round-trip estimates more accurate. Before the client's address is validated, a server may send at most three times the bytes it received, which stops QUIC from being used for amplification attacks; a Retry packet with a token can validate the address first under load. Connection IDs let a phone switch networks without a new handshake, and the endpoint probes the new path before trusting it. The costs: QUIC runs in user space, encryption is per packet, and UDP send and receive paths are less optimised than TCP, so CPU use per byte is higher. Kernel features like UDP GSO and larger socket buffers narrow the gap.

10Discovery, fallback and the real-world mix

A browser cannot know in advance that a server speaks HTTP/3. The common path: the first visit uses HTTP/2 over TCP, the response includes Alt-Svc: h3=":443", and the next connections race QUIC against TCP and use whichever works. The HTTPS DNS record can advertise h3 before the first connection, skipping that step. If UDP 443 is blocked or rate-limited by a firewall, the browser quietly falls back to TCP and may remember the failure for a while, so a misconfigured server rarely looks broken; it simply never gets HTTP/3 traffic. On the server, check that the firewall and any cloud security group allow UDP 443, that the same certificate is served on both listeners, and that load balancers forward UDP. In logs, record the protocol per request ($server_protocol in nginx) to see the real share of each version.

bash
# Ask for HTTP/3 directly (curl built with HTTP/3 support)
curl -sI --http3-only https://example.com/ | head -1
# Is h3 advertised?
curl -sI https://example.com/ | grep -i alt-svc
dig example.com HTTPS +short
# nginx log format: add the protocol per request
# log_format main '$remote_addr "$request" $status $server_protocol';

Real-world scenarios

01

HTTP/3 enabled, but browsers never use it

Problem

The nginx config has a quic listener and http3 on, and nginx starts fine. DevTools still shows h2 for every request, even after several reloads.

How to solve it

Walk the discovery path. Check that responses carry Alt-Svc with curl -sI; add_header inside a location block can override server-level headers, so make sure it appears on real pages. Confirm UDP 443 is open in ufw and in the cloud provider's firewall, and test from outside with curl --http3-only. Check nginx -V shows the QUIC module and that the OpenSSL build supports QUIC. Browsers cache the absence of HTTP/3 for a while, so test in a new profile. If a CDN is in front, HTTP/3 is the CDN's setting, not nginx's.

02

Mobile users on weak networks complain about slow loading

Problem

Analytics show that users on mobile networks in rural areas have much worse load times than Wi-Fi users, with frequent stalls on image-heavy pages.

How to solve it

Packet loss on these networks hits TCP-based HTTP/2 hard, because one lost packet stalls all streams. Serve HTTP/3 to visitors, either through a CDN or by enabling it on nginx with UDP 443 open. Then reduce what each page needs: compress images to AVIF or WebP with responsive sizes, lazy-load below the fold, and prioritise the LCP image. Fewer origins mean fewer handshakes. Compare real-user metrics by protocol and connection type before and after, rather than lab tests on fast Wi-Fi.

03

nginx fails to start after adding HTTP/3

Problem

After adding listen 443 quic reuseport to every virtual host, nginx -t reports a duplicate listen option and the reload fails.

How to solve it

reuseport may appear only once per address and port across the whole configuration. Keep listen 443 quic reuseport in one server block, often the default server, and use plain listen 443 quic in the others. Run nginx -t before each reload. While there, make sure every server block that should offer HTTP/3 also has http3 on and sends Alt-Svc, and that ssl_protocols includes TLSv1.3, which QUIC requires.

Cheat sheet

Which protocol does a site use?
curl -sI -o /dev/null -w "%{http_version}\n" https://example.com/
Force HTTP/2
curl -sI --http2 https://example.com/ | head -1
Force HTTP/3 only
curl -sI --http3-only https://example.com/ | head -1
Check ALPN negotiation
openssl s_client -connect example.com:443 -servername example.com -alpn h2 </dev/null 2>/dev/null | grep -i alpn
Enable HTTP/2 in nginx 1.25.1+
http2 on;
HTTP/3 listener in nginx
listen 443 quic reuseport; http3 on;
Advertise HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400' always;
Open UDP 443
sudo ufw allow 443/udp
Check nginx has QUIC support
nginx -V 2>&1 | tr ' ' '\n' | grep -i v3

In my real projects

  • I run nginx as the web server and reverse proxy on the production servers I manage, where HTTP/2 over TLS is part of the standard setup.
  • Cloudflare in front of my sites serves HTTP/2 and HTTP/3 to visitors while the origin stays on standard HTTPS.
  • The realtime parts of my products (socket.io, Pusher) run on long-lived connections through the same nginx layer.

Questions & answers

Q1What are the main differences between HTTP/1.1, HTTP/2 and HTTP/3?

HTTP/1.1 is text-based and handles one request at a time per connection, so browsers open several connections and slow responses block others. HTTP/2 uses binary frames and multiplexes many streams over one TCP connection, with HPACK header compression, removing HTTP-level head-of-line blocking. HTTP/3 keeps the same idea but runs over QUIC on UDP: streams are independent at the transport level, TLS 1.3 is built in so setup takes fewer round trips, and connections survive network changes. The semantics, meaning methods, status codes and headers, are the same in all three.

Q2What is head-of-line blocking?

It is when one delayed item holds up everything queued behind it. In HTTP/1.1 it happens at the application level: responses on a connection come in order, so a slow one blocks the rest. HTTP/2 fixes that with multiplexed streams, but TCP still delivers bytes strictly in order, so a single lost packet stalls all streams until it is retransmitted. That is transport-level head-of-line blocking, and it hurts on lossy mobile networks. HTTP/3 solves it with QUIC, where each stream is delivered independently, so a loss only delays the stream it belongs to.

Q3Why is HTTP/3 built on UDP rather than TCP?

TCP is implemented in operating system kernels and inspected by middleboxes everywhere, so changing its behaviour takes many years. UDP is a thin layer that passes through most networks, so QUIC can implement reliability, congestion control, streams and encryption in user space and evolve quickly. Building on UDP also makes independent streams possible, avoiding TCP's in-order delivery problem, and lets TLS 1.3 merge into the transport handshake. Connection IDs replace the IP and port tuple, so connections survive network changes. The cost is more CPU use and the need to fall back when UDP is blocked.

Q4Do you need HTTP/3 on the origin if you use Cloudflare?

No. Cloudflare terminates the visitor's connection at its edge, where it speaks HTTP/3 or HTTP/2 to the browser. The connection from Cloudflare to your origin is a separate hop over TCP using HTTP/1.1 or HTTP/2, usually over warm, reused connections in well-connected networks, where HTTP/3's benefits are small. So on the origin I make sure TLS is correct for Full (strict) and HTTP/2 is enabled, and leave HTTP/3 to the edge. HTTP/3 on your own nginx matters when visitors connect directly without a CDN.

Q5Which old performance optimizations should you stop using with HTTP/2?

Domain sharding, spreading assets across several hostnames to get more parallel connections, now hurts because each hostname needs its own DNS lookup, TCP and TLS handshake, and it breaks multiplexing and prioritization. Image sprites and aggressive concatenation into one huge bundle are less useful, because many requests are cheap and a giant file hurts caching: any change invalidates everything. Inlining many assets into HTML has a similar caching cost. Instead I split code by route, give assets hashed names and long cache lifetimes, and keep the number of origins small.

Senior-level questions

S1Can HTTP/2 make a site slower than HTTP/1.1?

Yes, in specific conditions. On lossy networks, one TCP connection carrying everything suffers when a packet is lost, while six HTTP/1.1 connections spread the risk and have six congestion windows. Broken prioritization, for example a proxy that ignores priorities, can let a large image or script hog the connection before CSS. Leftover HTTP/1.1 tricks like domain sharding force extra connections and defeat multiplexing. And very small initial flow-control windows can limit throughput on long, fast links. In practice HTTP/2 is faster for typical sites, but these cases explain odd regressions. HTTP/3 solves the loss problem, and correct priorities plus fewer origins solve most of the rest.

S2What operational costs come with running HTTP/3 yourself?

You add a UDP listener, so firewalls, cloud security groups and load balancers must allow and forward UDP 443, and many managed load balancers handle it differently from TCP. CPU usage per byte is higher than TCP with kernel offloads, which matters at high traffic. Debugging is harder because packets are encrypted: tcpdump shows little, so you rely on qlog files, server logs and browser tools. You must test fallback, because some networks block UDP. nginx support requires a QUIC-capable TLS library and careful reuseport configuration. For most teams the pragmatic choice is to let a CDN speak HTTP/3 to users and keep HTTP/2 on the origin, unless traffic goes directly to your servers.

S3How do gRPC and WebSockets relate to HTTP versions?

gRPC is defined on HTTP/2: each call is a stream, messages are length-prefixed Protocol Buffers, and status travels in trailers. That is why browsers cannot use it directly and need gRPC-Web or a proxy, and why every proxy on the path must support HTTP/2 end to end, including trailers. WebSockets started as an HTTP/1.1 Upgrade handshake; RFC 8441 added WebSockets over HTTP/2 via extended CONNECT, and RFC 9220 does the same for HTTP/3, though many servers and proxies still use the HTTP/1.1 upgrade. For nginx this means proxying WebSockets with the Upgrade and Connection headers and long timeouts. WebTransport is the HTTP/3-native option for low-latency streams and datagrams.

Glossary

Multiplexing
Sending many requests and responses at the same time over one connection, interleaved as frames.
Stream
One request–response exchange inside an HTTP/2 or HTTP/3 connection, with its own ID.
Head-of-line blocking
One delayed item holding up everything queued behind it, at the HTTP or transport layer.
HPACK / QPACK
Header compression schemes for HTTP/2 and HTTP/3 that avoid resending repeated headers.
ALPN
Application-Layer Protocol Negotiation: a TLS extension where client and server agree on h2 or http/1.1.
QUIC
A UDP-based transport with built-in TLS 1.3, independent streams and connection migration; HTTP/3 runs on it.
Alt-Svc
A response header telling the browser the same site is available over another protocol, such as h3.
0-RTT
Sending data in the very first packet of a resumed connection; fast but replayable, so only for safe requests.
Connection ID
QUIC's identifier for a connection, independent of IP and port, enabling network changes without reconnecting.
Flow control
A receiver-advertised limit on how much data the sender may send before waiting.

Have a product in mind?Let's build it.

Tell me about your idea, your product or your team. I usually reply within one working day.