HTTP/3 + QUIC Simulator

HTTP/3 runs HTTP over QUIC — a UDP-based transport that finally fixes the residual head-of-line blocking of HTTP/2. Press drop a packet below and watch HTTP/2 stall every stream while HTTP/3 keeps the others moving.

tick
0
h2 done
h3 done

HTTP/2 over TCP
one connection, one loss → every stream stalls
0 / 5000 B
stream 1 index.html 0 / 800 B
stream 3 app.css 0 / 1400 B
stream 5 bundle.js 0 / 2200 B
stream 7 hero.webp 0 / 600 B
HTTP/3 over QUIC
independent streams · loss isolated to one stream
0 / 5000 B
stream 1 index.html 0 / 800 B
stream 3 app.css 0 / 1400 B
stream 5 bundle.js 0 / 2200 B
stream 7 hero.webp 0 / 600 B

What you're looking at

Two stacks of the same four downloads racing in parallel: HTTP/2 over TCP on top, HTTP/3 over QUIC below. Each bar is one stream filling toward its byte total, and both protocols draw from the same fixed bandwidth budget each tick, so a fair comparison comes down to how each one handles a lost packet. Run starts the clock; Step advances one tick; the meter up top shows the tick count and a check mark when each side finishes.

The button to press is Drop a packet, ideally early in the run. The same loss hits both stacks at once. Watch the HTTP/2 lane freeze completely for six ticks. TCP delivers bytes in order, so one missing packet stalls every stream behind it, even the streams that have nothing to do with the loss. The HTTP/3 lane stalls only the one affected stream while the other three split the freed bandwidth and keep moving. What should surprise you is the size of the gap from a single drop: HTTP/3 crosses the line several ticks ahead, and that margin is exactly the head-of-line penalty TCP can't shed.


What HTTP/3 actually solves

The last mile of head-of-line blocking.

HTTP/3 is the third major version of the Hypertext Transfer Protocol, standardised in RFC 9114 (June 2022). Underneath HTTP/3 sits QUIC, a transport protocol that replaces TCP with a UDP-based stack (RFC 9000, May 2021). The combination ships HTTP semantics over a transport that was designed from scratch for the modern web — encrypted by default, multiplexed without app-layer blocking, and free of TCP's in-order delivery constraint that turned out to be HTTP/2's bottleneck on real networks.

HTTP/2 already fixed the obvious problem with HTTP/1.1: stop opening six TCP connections per origin and just multiplex many streams over one. But one TCP socket means one ordered byte stream. When the network drops a packet, the TCP receiver buffers everything that arrives after it until the lost packet is retransmitted — even bytes that belong to a completely different HTTP/2 stream. The application is blocked until the missing byte arrives. This is TCP head-of-line blocking, and on a 1% packet-loss mobile link it costs the median user hundreds of milliseconds per page load.

QUIC's defining move: track each stream's loss recovery independently at the transport layer. A dropped packet on stream 3 stalls stream 3 only; streams 1, 5, 7 keep moving. The simulator above is the simplest possible demonstration. Hit drop a packet mid-run and watch HTTP/2's lane freeze entirely for six ticks while HTTP/3 freezes a single stream and lets the others split the bandwidth.


QUIC's place in the stack

HTTP semantics over a brand-new transport.

In the TCP-era stack, HTTP/2 sat on TLS sat on TCP sat on IP. Four layers, each with its own connection setup, each with its own ordering and retransmission semantics. QUIC collapses three of those into one protocol that lives in userspace over UDP:

HTTP/1.1 / HTTP/2 stack          HTTP/3 stack
┌────────────────────┐            ┌────────────────────┐
│  HTTP semantics    │            │  HTTP/3 (RFC 9114) │
├────────────────────┤            ├────────────────────┤
│  TLS 1.3           │            │  QUIC streams      │
├────────────────────┤            │  + TLS 1.3 (built  │
│  TCP               │            │    in, RFC 9001)   │
├────────────────────┤            ├────────────────────┤
│  IP                │            │  UDP               │
└────────────────────┘            ├────────────────────┤
                                  │  IP                │
                                  └────────────────────┘

The collapse matters because each old boundary cost a round trip on setup. TCP needs a SYN → SYN-ACK → ACK handshake before anything moves; TLS 1.3 needs a ClientHello → ServerHello → Finished exchange on top. Cold-start was 2-3 RTTs before the first HTTP byte. QUIC does its transport handshake and TLS handshake in the same round trip — the QUIC Initial packet carries the ClientHello, the server response carries everything the client needs to send application data immediately. One RTT, full stop.

Why UDP? Two reasons. First, every middlebox on the internet — NAT gateways, firewalls, load balancers — knows TCP and UDP. A new IP-layer protocol couldn't traverse them; even after a decade of adoption it'd still be blocked on enterprise networks. UDP is the lowest-common-denominator wrapper that lets QUIC reach the internet without infrastructure changes. Second, UDP doesn't impose ordering or retransmission — exactly what QUIC needs to provide per-stream rather than per-connection.


1-RTT, 0-RTT, and the cold-start gap

Where the milliseconds go.

A cold-start HTTP request over TCP + TLS 1.3 + HTTP/1.1 costs the client roughly three round trips before the first byte of response arrives. The TCP three-way handshake takes one RTT. TLS 1.3 adds one more (the famously short version — TLS 1.2 cost two). Then the HTTP request goes out and the response comes back: a third RTT before meaningful data lands. On a 100 ms RTT (typical inter-continental), that's 300 ms of pure setup.

QUIC does setup and TLS in one combined exchange. The Initial packet from the client carries the QUIC version, source connection ID, and the TLS ClientHello inside QUIC's CRYPTO frame. The server's response — also Initial + Handshake packet — carries the ServerHello, the certificate, and the Finished message. Total: 1 RTT. After that, application data flows.

Then there's 0-RTT. A returning client that has a TLS session ticket from a prior connection can send its first application request in the very first packet, using keys derived from the resumed session. The server responds with both the Handshake completion AND the application data response. Zero round trips before the first useful byte. On a 100 ms RTT link, this is a straight ~200-300 ms win on every repeat visit.

ScenarioRTTs to first byteNotes
TCP + TLS 1.3 + HTTP, cold3SYN handshake + TLS handshake + request/response
TCP + TLS 1.3 + HTTP, warm (session resumed)2TLS 1.3 keeps a half-RTT improvement; TCP still costs 1
QUIC + HTTP/3, cold1Combined transport + TLS handshake; request rides 0.5 RTT in
QUIC + HTTP/3, 0-RTT (resumed)0Request in first packet, response on the way back
The 0-RTT replay caveat. An attacker who captures a 0-RTT packet can replay it. The standard mitigation is to restrict 0-RTT to idempotent requests at the server — typically GET only. Mutating verbs (POST, PUT, DELETE) require a full 1-RTT handshake. Cloudflare, Fastly, and Google all enable 0-RTT for GETs by default; the safety net is the per-method gate at the server.

Survive the Wi-Fi → cellular switch

The connection follows the user, not the IP.

TCP identifies a connection by the 4-tuple (source IP, source port, destination IP, destination port). Change any field — say your phone drops Wi-Fi and switches to LTE — and the kernel sees a new TCP connection. Your old connection is dead; any in-flight HTTP request fails; the application has to retry on a fresh socket, paying the full handshake cost again.

QUIC identifies a connection by a Connection ID — a server-assigned, network-independent identifier carried in every packet header. When your phone's IP changes, the server sees an incoming packet from a new (IP, port) pair but recognises the Connection ID and continues the existing session. The path is validated with a small PATH_CHALLENGE / PATH_RESPONSE exchange (one RTT), and traffic resumes. No application-visible failure.

This is the killer feature for mobile. A typical commuting day involves dozens of network transitions — Wi-Fi at home, cellular on the walk to the train, Wi-Fi at the office. Under TCP, every transition kills every open connection. Under QUIC, the connection survives. YouTube, Google Maps, Gmail, and every Cloudflare-fronted site already use this in production.


Inside a QUIC packet

Long header, short header, and the frame payload.

Every QUIC packet has either a long header (used during the handshake to carry version + connection IDs explicitly) or a short header (used after the handshake when both ends already know the connection IDs). The short header is one byte of flags + the destination connection ID + the packet number + the encrypted payload — under 20 bytes of envelope around arbitrary frames.

QUIC SHORT HEADER PACKET (post-handshake, the common case)
+--------+--------------------+-----------+-------------------+
| Flags  | Dest Connection ID | Pkt Num   | Encrypted Payload |
| 1 byte | 8 bytes (typical)  | 1-4 bytes | varies            |
+--------+--------------------+-----------+-------------------+
                                              │
                                              ▼
                          ┌──────────────────────────────────┐
                          │  Frames (STREAM, ACK, MAX_DATA,  │
                          │   PING, PATH_CHALLENGE, etc.)    │
                          └──────────────────────────────────┘

STREAM FRAME (the workhorse — carries application data)
+-------+----------+-------+-------+-------------------------+
| Type  | StreamID | Off   | Len   | Stream Data             |
| 1B    | varint   | varint| varint| (the actual bytes)      |
+-------+----------+-------+-------+-------------------------+

The frame layer is where QUIC's stream multiplexing lives. A single QUIC packet can carry frames for many streams interleaved — exactly like HTTP/2's frame layer, except the loss-recovery boundary is the individual STREAM frame rather than the TCP segment. If packet 42 carries STREAM frames for streams 1, 3, and 5, and packet 42 is lost, only those three streams have any data to retransmit — and the retransmit only affects those three streams' progress, not the whole connection.

Frame typePurpose
STREAMApplication data for a numbered stream. The workhorse.
ACKAcknowledgement with packet number ranges + ack delay.
CRYPTOTLS handshake data during connection setup.
MAX_DATA / MAX_STREAM_DATAFlow-control credit updates.
NEW_CONNECTION_IDServer hands the client more CIDs for path migration.
PATH_CHALLENGE / PATH_RESPONSEValidate a new network path during migration.
RESET_STREAMAbort a stream (with error code).
PINGLiveness probe.
CONNECTION_CLOSETear down the connection.

When HTTP/3 isn't faster

The wins come from lossy + high-RTT networks.

Three honest caveats about where HTTP/3 doesn't beat HTTP/2 in practice:

  • Reliable, low-RTT networks. On a wired desktop with 10 ms RTT and 0% packet loss, the per-stream loss recovery doesn't fire because nothing is being lost. HTTP/2 over TLS-resumed TCP and HTTP/3 over QUIC both achieve effectively identical page-load times. The win is in mobile + lossy + high-RTT scenarios — exactly where HTTP/2 was already pretty good but not great.
  • UDP throttling. Some enterprise networks and a handful of mobile carriers rate-limit UDP traffic more aggressively than TCP, treating it as "probably gaming or video, not important." Where this happens, HTTP/3 measures slower than HTTP/2. The trend is improving — most major carriers have stopped — but it's still a real edge case.
  • CPU on the server. QUIC does TLS in userspace. TCP+TLS benefits from kernel TLS (kTLS) on Linux, which can use AES-NI directly and even offload to NIC hardware. A high-throughput static-file server can serve HTTP/2 at lower CPU than HTTP/3 — sometimes 30-50% lower at the same Gbps. Workarounds keep landing (eBPF-accelerated QUIC, hardware QUIC offload), but the gap hasn't closed — benchmark your own stack rather than assuming parity.
  • Static assets fronted by CDN. Cloudflare, Fastly, and Akamai all serve static assets over HTTP/3 by default. The end-user win is real (especially mobile) but tracing it back to your origin's HTTP version is misleading — your origin can be HTTP/1.1 and the user can still get HTTP/3 to the CDN edge.
The decision tree. Enable HTTP/3 at your edge. Track p95 page-load latency by network type before and after. If mobile improves and desktop doesn't change, that's the canonical HTTP/3 win. If you see desktop regress, check whether your TLS stack is using kTLS — switching back to HTTP/2 over kTLS for static high-throughput paths is a valid hybrid setup.

Servers, libraries, and how to turn it on

Production-ready stacks, 2026.

StackHTTP/3 supportNotes
NGINX1.25.0+ (mainline 2023), built-inCompile with --with-http_v3_module; production-ready.
Caddy2.0+, defaultHTTP/3 + 0-RTT enabled out of the box.
Envoy1.20+ (QUIC enabled 1.25)Used by Istio service mesh.
HAProxy2.6+Listener config; production since 2.7.
CloudflareDefault on every siteQuiche library (Rust) — open source.
FastlyDefault on every sitequicly (C) and h2o.
Google Cloud Load BalancerDefaultUsed by google.com, youtube.com since 2017 (gQUIC, now IETF QUIC).
AWS CloudFrontAvailable, opt-inEnable in distribution settings.
Go (net/http3)quic-go libraryNot in std lib; widely used in production.
Rustquiche, quinnTwo mature, production-grade libraries.
Node.jsnode:quic experimentalAvailable behind a flag; not yet stable.
BrowsersChrome, Firefox, Safari all ship itAuto-upgrade on Alt-Svc header from the server.

The standard discovery mechanism is the Alt-Svc response header. A server speaking both HTTP/2 and HTTP/3 sets Alt-Svc: h3=":443"; ma=86400 on its HTTP/2 responses; browsers cache that hint and switch to HTTP/3 for the next connection to the same origin. There's no DNS coordination, no service-discovery dance — just a header.


References

Found this useful?