Networking

QUIC

A reliable transport on UDP that fixes TCP's mistakes

QUIC is a transport protocol over UDP that bakes TLS 1.3 into the wire, multiplexes streams without TCP head-of-line blocking, and sets up in 1 RTT (or 0 with resumption). It's the foundation of HTTP/3.

  • RFC9000 (2021)
  • TransportUDP
  • Setup1 RTT (0 RTT resume)
  • EncryptionTLS 1.3 baked in
  • Per-stream loss recoveryYes

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How QUIC works

QUIC is what TCP would look like if it were designed in 2016 instead of 1981. It runs as application bytes inside UDP datagrams, but inside the UDP payload it implements every reliable-transport feature TCP has — sequencing, retransmission, flow control, congestion control — plus the ones TCP lacks: encryption-by-default, multiplexed streams without head-of-line blocking, and connection migration across IP changes.

A QUIC packet, after the UDP header, looks like:

+---------------+
| Header Form   |  short or long header
+---------------+
| Version       |  long-header packets only (handshake)
+---------------+
| Connection ID |  8-20 byte opaque identifier
+---------------+
| Packet Number |  per-direction monotonic, encrypted
+---------------+
| Frames...     |  STREAM, ACK, CRYPTO, NEW_CONNECTION_ID,
|               |  RESET_STREAM, MAX_DATA, PING, etc.
+---------------+

Almost all of that is encrypted. The "long-header" form is used during handshake; once the keys are established, packets switch to the compact "short header" form (Connection ID + Packet Number + frames). Inside, QUIC is frame-oriented: a single packet can carry STREAM frames for several streams plus an ACK frame plus flow-control updates.

The handshake interleaves transport setup and TLS 1.3:

  1. Client sends an Initial packet with a CRYPTO frame containing TLS ClientHello.
  2. Server replies with Initial (containing ServerHello + EncryptedExtensions + Certificate + Finished). After this single round trip, both sides have the application-data keys.
  3. Client sends 1-RTT data — encrypted with the new keys.

That's 1 RTT to first byte, total. With session resumption (the client cached the server's transport parameters and a PSK), the client packs application data into the very first Initial — 0 RTT.

QUIC vs TCP+TLS

TCP + TLS 1.2TCP + TLS 1.3QUIC + TLS 1.3 (HTTP/3)
RTTs to first byte (cold)321
RTTs to first byte (resumed)210
MultiplexingOne streamOne streamMany streams, independent
HoL blocking on lossYes (whole connection)Yes (whole connection)Per-stream only
Header encryptionNoneNoneYes (packet number etc.)
Connection identifier4-tuple (src/dst IP+port)4-tupleConnection ID — IP change OK
Migration across networksRST and reconnectRST and reconnectSeamless
ImplementationKernel + libsslKernel + libsslUserspace (mostly)
Middlebox visibilitySequence, window, ACK visibleSameOnly Connection ID + version

The userspace point matters: TCP changes ship as kernel updates, so deployment of new congestion control or new options is measured in years. QUIC ships as a library — Cloudflare's quiche, Google's quic-go, ngtcp2, msquic — and a server can roll out a new congestion controller in a deploy.

JavaScript implementation — HTTP/3 client with node:net?

Node added experimental QUIC support in v20+ via node:quic. As of 2026, most Node code uses undici's HTTP/3 client built on the same underlying library:

// Send a request over HTTP/3 (QUIC) using undici
import { request, Agent } from 'undici';

const agent = new Agent({
  allowH2: true,         // permit HTTP/2 fallback
  allowH3: true,         // try HTTP/3 first if Alt-Svc advertises h3
});

const { statusCode, headers, body } = await request(
  'https://www.cloudflare.com/cdn-cgi/trace',
  { dispatcher: agent }
);

console.log('alpn:', headers[':alpn']);  // expect "h3"
for await (const chunk of body) process.stdout.write(chunk);

The "negotiation" is invisible: the first request goes over HTTP/2; the response includes an Alt-Svc: h3=":443" header; subsequent requests upgrade to HTTP/3 over QUIC. The client caches the Alt-Svc record and races HTTP/3 against HTTP/2 on later visits.

Python implementation — 0-RTT with aioquic

"""
Connect to an HTTP/3 server, save the session ticket, then reconnect
using 0-RTT — application data goes in the very first packet.
"""
import asyncio
from aioquic.asyncio import connect
from aioquic.quic.configuration import QuicConfiguration
from aioquic.h3.connection import H3Connection
from aioquic.h3.events import HeadersReceived, DataReceived

async def fetch(host, port, path, session_ticket=None):
    config = QuicConfiguration(
        is_client=True,
        alpn_protocols=["h3"],
    )
    if session_ticket:
        config.session_ticket = session_ticket   # enables 0-RTT

    async with connect(host, port, configuration=config) as protocol:
        h3 = H3Connection(protocol._quic)
        stream_id = protocol._quic.get_next_available_stream_id()
        h3.send_headers(stream_id, [
            (b":method", b"GET"),
            (b":scheme", b"https"),
            (b":authority", host.encode()),
            (b":path", path.encode()),
        ], end_stream=True)
        protocol.transmit()
        # ... await response, return ticket for reuse ...

# First call: 1-RTT handshake, server returns a session ticket
ticket1 = asyncio.run(fetch("cloudflare-quic.com", 443, "/"))
# Second call: 0-RTT — request flies in the very first datagram
asyncio.run(fetch("cloudflare-quic.com", 443, "/", session_ticket=ticket1))

0-RTT shaves a full RTT — meaningful at 200ms transcontinental — but the trade-off is replay. An attacker who recorded the first 0-RTT packet can resend it to a different server instance and the request executes again. Servers must mark replay-unsafe endpoints (anything that mutates state) as "early-data: rejected" in the application logic.

Concrete costs and gains

  • Setup latency: 1 RTT vs TCP+TLS 1.3's 2 RTT. On a 200 ms transatlantic link, that's 200 ms saved on every cold connection. With 0-RTT, 400 ms saved on warm.
  • Page-load impact: Cloudflare and Google measured median page load 5-20% faster on HTTP/3 vs HTTP/2 across global traffic, with the gain concentrated in long-RTT and lossy networks.
  • Mobile improvement: ~30% reduction in tail-latency video rebuffer events when a phone roams between Wi-Fi and cellular, thanks to connection migration.
  • CPU cost: ~2× higher CPU than TCP for equivalent throughput in 2024 measurements (userspace + per-packet crypto). 2026 stacks with hardware crypto offload narrowed the gap to ~1.3×.
  • Network amplification protection: a server may not send more than 3× what it received until the client's address is validated, preventing UDP amplification.
  • UDP path quality: ~2-5% of users have a path that drops or rate-limits UDP. Browsers always race HTTP/3 against HTTP/2 and fall back transparently.

Variants and extensions

  • HTTP/3 (RFC 9114) — the obvious application of QUIC. QPACK (RFC 9204) replaces HPACK to handle stream-independent header compression.
  • MASQUE (RFC 9298, 9484) — Multiplexed Application Substrate over QUIC Encryption. Tunnels arbitrary protocols (UDP, IP, ConnectIP) inside QUIC streams. Apple's Private Relay and Cloudflare's WARP use MASQUE for VPN-like proxying.
  • Multipath QUIC (RFC 9440 + drafts) — a single QUIC connection across multiple network paths simultaneously (Wi-Fi + cellular). Improves robustness and aggregate throughput.
  • Datagram extension (RFC 9221) — unreliable, unordered DATAGRAM frames within a QUIC connection. Used by WebTransport and MASQUE for media without full retransmit.
  • WebTransport — browser API for bidirectional streams + datagrams over QUIC, exposed to JavaScript. Replaces WebSockets where you want unreliable channels alongside reliable ones.
  • QUIC v2 (RFC 9369) — version negotiation hardening: the version-1 handshake explicitly carried unencrypted bytes that ossified into middleboxes. v2 alters those bytes to discourage middlebox parsing of QUIC internals.
  • Pluggable congestion control — Cubic, BBR, and even custom controllers ship per implementation. Some servers run BBRv3 by default.

Common bugs and edge cases

  • Middlebox interference. Some firewalls rate-limit UDP/443 or drop fragmented UDP. Symptoms: HTTP/3 negotiations succeed at low load and fail at scale, or work for short connections and break for long-lived ones. Mitigation: cap QUIC payloads at 1200 B; always offer HTTP/2 as a fallback.
  • 0-RTT replay attack. A captured 0-RTT packet can be replayed to any server instance. RFC 9001 mandates that replayable methods (GETs of cacheable resources) are 0-RTT-safe; everything else must be deferred to 1-RTT data. Early-Data request headers and 425 Too Early responses are part of this.
  • Connection ID exhaustion. Each side can announce up to active_connection_id_limit CIDs. A migrating client uses fresh CIDs to thwart linkability; running out causes the connection to fail to migrate. Servers must keep handing out new CIDs as old ones retire.
  • UDP rate-limit at the kernel. Linux's default net.core.rmem_default is 208 KB. A burst of 1500 B QUIC packets fills it; the rest are dropped silently. High-throughput QUIC servers bump SO_RCVBUF and use recvmmsg to drain in bulk.
  • Anti-amplification limit failure. A server replying to an unverified client must not send more than 3× what it received. Implementations that ignore this can be turned into reflectors. The validation is via the client's first NEW_CONNECTION_ID + ACK round trip.
  • NAT rebinding mid-handshake. A client's source port changes (mobile Wi-Fi handoff) before the handshake completes; the server's address validation token is now invalid; the connection drops. RFC 9000 specifies path validation challenges to recover.
  • Spurious version negotiation. If the server doesn't recognize the client's QUIC version, it sends a Version Negotiation packet. Older clients incorrectly treated this as a connection close. Modern stacks retry with a supported version.

Frequently asked questions

Why does QUIC ride on UDP instead of replacing TCP?

Middleboxes. Routers, NATs, and firewalls deployed in the last 30 years know UDP and TCP. A new IP-level protocol number gets dropped or mangled by a meaningful fraction of the path. UDP is the lowest layer where new transports can actually be deployed.

How fast is QUIC's handshake?

1 RTT for the first connection — TLS 1.3 keys are negotiated in the same round trip that establishes the transport. With session resumption, 0 RTT: the client sends application data in its very first packet using a previously-cached key. TCP+TLS 1.3 needs 2 RTT minimum; TCP+TLS 1.2 needs 3.

Does QUIC eliminate head-of-line blocking?

Yes — at the transport. Each QUIC stream has its own sequence space and loss-recovery state, so a lost packet on stream A does not stall stream B. Crucially, the encryption is per-packet, not stream-spanning, so out-of-order packets can be decrypted independently. HTTP/3 inherits this property.

What is connection migration?

QUIC connections are identified by a Connection ID, not a (src IP, src port, dst IP, dst port) 4-tuple. When a phone moves from Wi-Fi to 5G, the IP changes but the Connection ID does not — QUIC continues seamlessly. TCP would reset and force a fresh handshake.

Is QUIC encrypted end-to-end?

Almost everything is encrypted, including the transport headers. Only the version, source/destination Connection IDs, and a few unprotected handshake bits are visible. This means middleboxes can no longer observe sequence numbers, ACKs, or flow control — by design.

What's the relationship to HTTP/3?

HTTP/3 is HTTP semantics over QUIC. The QUIC streams replace HTTP/2's frame-on-TCP layer. Most browsers, CDNs, and Google services route ~60-75% of web traffic over HTTP/3 in 2026. The transport (QUIC) and the application (HTTP) are now formally split.