QUIC#

QUIC (RFC 9000) is the transport protocol underneath HTTP/3. Unlike everything else on this site, QUIC runs on UDP, the same UDP that’s been quietly carrying DNS and game traffic for decades, but rebuilds reliability, ordering, encryption, and multiplexing at the application layer. Originated at Google, run by Google / Cloudflare / Meta / Akamai, and standardized in 2021; QUIC carries a large share of public web traffic. Operators encounter it in three places: load balancers (HTTP/3 front ends), client traffic (browser → CDN), and capture analysis (it looks like UDP/443 noise unless you know what to do).

What QUIC is, in one paragraph: TCP + TLS 1.3 + multiplexing, moved into userspace and run over UDP. The handshake is one round trip (or zero with 0-RTT resumption). Streams are independent; loss on stream A doesn’t block stream B (no head-of-line blocking, HTTP/2’s chronic problem on TCP). Connections survive an IP / port change (mobile networks): the connection ID stays the same. The whole transport is encrypted, including most of the header.

Packet headers#

QUIC carries two header formats. The long header is used during the handshake and version negotiation; it carries explicit version, DCID, and SCID fields. The short header is used once the handshake completes; most of it is encrypted, and only the destination connection ID is visible to the wire.

        packet-beta
    title QUIC Long Header (handshake)
    0: "H=1"
    1: "F=1"
    2-3: "Type"
    4-7: "Type-specific"
    8-39: "Version"
    40-47: "DCID Len"
    48-207: "Destination Connection ID (variable)"
    208-215: "SCID Len"
    216-375: "Source Connection ID (variable)"
    
        packet-beta
    title QUIC Short Header (1-RTT)
    0: "H=0"
    1: "F=1"
    2: "S"
    3: "R"
    4: "R"
    5: "K"
    6-7: "PN Len"
    8-167: "Destination Connection ID (variable)"
    168-199: "Packet Number (variable)"
    200-263: "Protected Payload"
    

The handshake#

QUIC + HTTP/3 versus the older TCP + TLS 1.2 + HTTP/2 stack:

Property

QUIC + HTTP/3 TCP + TLS 1.2 + HTTP/2

Transport

UDP/443 TCP/443

Encryption

TLS 1.3 baked in TLS 1.2 / 1.3 layered on

Handshake RTTs

1 (or 0-RTT resumed) 2 (TCP) + 1-2 (TLS)

Multiplexing

Yes, no head-of-line block Yes, but blocked by TCP loss

Connection migration

Yes (connection ID) No (5-tuple bound)

Header encryption

Most of it None

Userspace stack

Yes Kernel TCP + userspace TLS

        sequenceDiagram
    participant C as Client
    participant S as Server (UDP/443)
    C->>S: Initial (ClientHello, key share, ALPN h3)
    S->>C: Initial (ServerHello, cert, key share, finished)
    Note over C,S: 1-RTT handshake complete
    C->>S: Handshake (finished) + 0-RTT app data (if resuming)
    C->>S: Application: HTTP/3 frames over QUIC streams
    S->>C: Application: HTTP/3 response
    

Servers + clients#

Tool

Use

nginx (>=1.25)

Native HTTP/3 listener (listen 443 quic reuseport;).

caddy

HTTP/3 by default since v2.6.

haproxy (>=2.6)

QUIC front-end via OpenSSL-QUIC or quictls.

envoy

HTTP/3 listener (udp_listener_config).

quiche-server

Cloudflare reference server.

msquic

Microsoft’s QUIC stack (also in Linux).

curl --http3

Operator’s first probe (needs a curl built with nghttp3 / ngtcp2 or quiche).

ngtcp2, quiche

Library + reference clients.

quictls / BoringSSL

TLS forks with QUIC API support.

$ curl -I --http3 https://cloudflare.com
$ curl -v  --http3 https://cloudflare.com 2>&1 | grep -E 'ALPN|HTTP'

# quiche reference client
$ quiche-client https://cloudflare-quic.com/

# check whether your curl has http3 enabled
$ curl --version | grep -o 'HTTP3'

Capture + analyze#

QUIC traffic is just UDP/443 on the wire. Without keys, there’s nothing to read past the first few bytes.

# capture to a pcap
$ sudo tcpdump -i any -nn -w quic.pcap 'udp port 443'

# decrypt in Wireshark: set SSLKEYLOGFILE before launching the client
$ export SSLKEYLOGFILE=/tmp/keys.log
$ curl --http3 https://example.com
# then in Wireshark: Edit -> Preferences -> Protocols -> TLS ->
# (Pre)-Master-Secret log filename: /tmp/keys.log

# nginx access log shows http2 vs http3
$ awk '{print $NF}' /var/log/nginx/access.log | sort -u | head

Firewall and ops gotchas#

  • UDP/443 must be open both ways. Many corporate egress proxies block UDP except DNS/NTP; QUIC silently fails over to TCP/TLS. Alt-Svc header is how the server advertises HTTP/3 availability; clients fall back if QUIC fails.

  • MTU and fragmentation. QUIC uses Path MTU Discovery and refuses to be fragmented. Black-holes show up as “first request works, larger ones hang.”

  • Stateless retry / connection ID breaks naive load balancers so you can’t 5-tuple hash. Use connection-ID-aware LBs (HAProxy proto quic, Envoy QUIC, Cloudflare).

  • Capture without keys is opaque. Plan for SSLKEYLOGFILE on endpoints you control, or terminate QUIC at a load balancer you can introspect.

  • HTTP/3 isn’t QUIC. HTTP/3 is one application of QUIC; SMB, DNS-over-QUIC (RFC 9250), and proprietary protocols also use it.

See also#

  • UDP, the transport QUIC rides on.

  • HTTP, the HTTP/1.1 / HTTP/2 / HTTP/3 progression.

  • SSL/TLS, TLS 1.3, which QUIC bakes in.

  • RFC 9000 (QUIC), RFC 9001 (TLS+QUIC), RFC 9002 (loss / congestion), RFC 9114 (HTTP/3).

  • cloudflare-quic.com, public test endpoint.