04 — QUIC Protocol
Technical Overview
QUIC is a multiplexed transport protocol built on UDP that solves fundamental limitations of TCP that are impossible to fix without widespread operating system updates. It implements connection establishment, flow control, loss recovery, and congestion control in user space — enabling rapid iteration on protocol design. QUIC's key innovations are: multiplexed streams without head-of-line blocking, 0-RTT connection resumption, connection migration across IP changes, and mandatory TLS 1.3 integration. QUIC is the transport for HTTP/3 and is increasingly used for non-HTTP applications including DNS-over-QUIC and QUIC-based RPC.
Prerequisites
- TCP state machine and 3-way handshake (see
01-tcp-state-machine.md) - TLS 1.3 handshake fundamentals (see
06-tls-internals.md) - UDP socket programming
- HTTP/2 multiplexing concepts (see
07-http-versions.md)
Core Content
Why QUIC Exists: TCP's Unfixable Problems
Problem 1: Head-of-Line Blocking at TCP layer
HTTP/2 multiplexes multiple streams over a single TCP connection to avoid connection overhead. But TCP is a byte stream without concept of messages or streams. When a TCP segment is lost:
HTTP/2 streams over TCP:
Stream 1: [seg1_ok][seg2_LOST][seg3_ok][seg4_ok] ...
Stream 2: [seg5_ok][seg6_ok][seg7_ok] ...
Stream 3: [seg8_ok][seg9_ok]
TCP receive buffer:
seg1_ok, [missing: seg2], seg3_ok, seg4_ok, seg5_ok, ...
|
v
TCP holds ALL data at seq > seg2 until seg2 is retransmitted
HTTP/2 cannot deliver stream 2 or stream 3 data
even though ALL their segments arrived
HTTP/2 solved application-layer HOL blocking (no more HTTP/1.1 pipeline queuing) but cannot solve TCP-layer HOL blocking. QUIC solves it by implementing streams at the transport layer: each QUIC stream has its own flow control, and stream A's loss doesn't block stream B.
Problem 2: Connection establishment latency
TCP + TLS 1.3 requires at minimum 1 network round trip before application data can flow:
Client Server
| SYN | \
|-------------->| | 1 RTT (TCP handshake)
| SYN-ACK | |
|<--------------| /
| ACK+ClientHello| \
|-------------->| | 1 RTT (TLS 1.3 handshake)
| ServerHello | |
| +Certificate | |
| +Finished | |
|<--------------| /
| Finished |
| HTTP Request | <-- application data on 3rd send
|-------------->|
Total: 2 RTTs before first HTTP request byte is sent. On 100ms RTT, that's 200ms before any application work.
Problem 3: Ossification
TCP headers are inspected and modified by every middlebox on the path — NAT boxes, firewalls, load balancers, traffic shapers. New TCP options are frequently blocked or stripped. TCP Fast Open (TFO) was deployed in 2014; years later, a significant fraction of networks block TFO SYN data. The Internet's middlebox ecosystem prevents TCP evolution.
QUIC runs inside UDP datagrams. Most middleboxes pass UDP on port 443 without inspection. The QUIC header that is visible to middleboxes is minimal; all transport state (stream IDs, flow control, packet numbers) is encrypted.
QUIC Over UDP
QUIC is not a UDP extension — it is a complete transport protocol that happens to use UDP as a substrate. QUIC is responsible for:
- Connection establishment (including TLS 1.3)
- Packet numbering and loss detection
- Flow control (connection-level and stream-level)
- Congestion control
- Stream multiplexing
- Connection migration
A QUIC packet in UDP:
UDP header (8 bytes)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Dest Port |
| Length | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
QUIC header (variable length, partially encrypted):
+-+-+-+-+-+-+-+-+
| Header Form | 1 bit: Long (1) or Short (0)
| Fixed Bit | always 1 (greasing)
| Packet Type | Initial / Handshake / 0-RTT / 1-RTT
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (Long header only) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID Length (8 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0–160 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (encrypted, 1–4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload (encrypted QUIC frames) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The Destination Connection ID is the critical field that enables connection migration.
QUIC Connection Establishment: 1-RTT and 0-RTT
1-RTT (first connection to a server):
Client Server
| Initial[0] CRYPTO(ClientHello) | \
|---------------------------------------->| |
| | | 1 RTT
| Initial[0] CRYPTO(ServerHello, | |
| Certificate, CertVerify, Finished) | |
|<----------------------------------------| /
| Handshake[0] CRYPTO(Finished) |
| 1-RTT[1] STREAM[0] HTTP request | <-- data on 2nd packet
|---------------------------------------->|
| 1-RTT[0] STREAM[0] HTTP response |
|<----------------------------------------|
QUIC combines the transport handshake and TLS 1.3 handshake into a single round trip. TLS 1.3's key_share in ClientHello allows the server to compute session keys immediately, without a second round trip for key exchange.
Compare to TCP + TLS 1.3: - TCP: 1 RTT (SYN+SYNACK+ACK) - TLS 1.3: 1 RTT (ClientHello → ServerHello+Certificate+Finished → client Finished) - Total: 2 RTTs - QUIC: 1 RTT total (transport + crypto combined)
0-RTT (session resumption):
On a subsequent connection to the same server, if the client has a Pre-Shared Key (PSK) from the previous session:
Client Server
| Initial[0] CRYPTO(ClientHello+PSK) |
| 0-RTT[0] STREAM[0] HTTP request | <-- data on FIRST packet
|---------------------------------------->|
| | (server verifies PSK, decrypts 0-RTT)
| 1-RTT[0] STREAM[0] HTTP response |
|<----------------------------------------|
Application data is sent with the very first packet — zero round trips to start transferring data.
0-RTT security caveat: 0-RTT data is vulnerable to replay attacks. An attacker who captures the 0-RTT packet can replay it against the server. HTTP GET requests are safe (idempotent); POST/PUT/DELETE must not use 0-RTT without application-level replay protection.
Stream Multiplexing Without HOL Blocking
QUIC streams are independent sequences of bytes identified by a stream ID. Stream IDs encode: - Client-initiated vs server-initiated (bit 0) - Bidirectional vs unidirectional (bit 1)
QUIC connection with 3 bidirectional streams:
Stream 0: [frame1][frame2][frame3_LOST] ... [retransmit_3][frame4]
Stream 2: [frame5][frame6][frame7] (unaffected by stream 0 loss)
Stream 4: [frame8][frame9] (unaffected)
Each stream has its own:
- Flow control (max_stream_data)
- Offset tracking
- Receive buffer
Connection-level flow control limits total across all streams.
When stream 0 loses frame3, only stream 0's delivery is stalled. Streams 2 and 4 continue delivering to the application. This is the fundamental HOL blocking fix that was impossible in TCP.
Connection ID and Migration
Each QUIC connection is identified by a Connection ID (DCID), not by the 4-tuple. This enables:
Connection migration: when the client's IP or port changes (Wi-Fi → cellular, NAT rebinding), the server receives a packet with the same DCID from a new source address. QUIC validates the new path and transparently migrates — no connection reset, no TLS renegotiation.
Client Server
| (on Wi-Fi: 192.168.1.5:50000) |
| DCID=abc123 STREAM data |
|---------------------------------------->|
| |
| (network change: 10.0.0.1:51234) |
| DCID=abc123 STREAM data |
|---------------------------------------->|
| Server recognizes DCID=abc123
| validates new path
| continues existing connection
TCP would send RST (new 4-tuple = new connection, old connection is dead).
Path validation: QUIC sends a PATH_CHALLENGE frame on the new path and requires a PATH_RESPONSE to confirm it's a legitimate path migration, not a hijack attempt.
QUIC Loss Detection
QUIC doesn't use sequence numbers in the TCP sense. Instead: - Each packet has a monotonically increasing packet number (never reused) - Retransmitted data gets a new packet number - This solves the TCP retransmit ambiguity (Karn's algorithm) — the ACK for a retransmitted packet unambiguously refers to the retransmit, not the original
Loss detection mechanisms: 1. ACK ranges: receiver ACKs are explicit ranges (not cumulative), enabling precise loss identification 2. Packet threshold: loss declared after 3 out-of-order packets (like TCP fast retransmit) 3. Time threshold: loss declared if packet not acknowledged after max(9ms, 1.125 * max_ack_delay)
# QUIC packet number space is per-epoch:
# - Initial epoch (unencrypted/Initial protection)
# - Handshake epoch (Handshake keys)
# - 1-RTT epoch (application data keys)
# Loss detection is independent per epoch
QUIC Congestion Control
QUIC uses the same congestion control algorithms as TCP: NewReno (default in many implementations), Cubic, and BBR. The algorithms receive the same inputs: packet loss events, RTT measurements, and bandwidth estimates.
QUIC's advantage: accurate RTT measurement. Since retransmitted packets have new packet numbers, there is no Karn ambiguity — RTT samples are taken from acknowledged packet numbers, including retransmits. This produces more accurate SRTT/RTTVAR, leading to better-calibrated RTO.
QUIC Implementations
| Implementation | Language | Used by |
|---|---|---|
| Google QUIC (gQUIC) | C++ (Chromium) | Google properties (pre-IETF QUIC) |
| quiche (Cloudflare) | Rust | Cloudflare edge, nginx-quic |
| lsquic (LiteSpeed) | C | LiteSpeed Web Server, Apache mod_quic |
| MSQUIC (Microsoft) | C | Windows, .NET, Azure |
| ngtcp2 | C | Reference IETF QUIC |
| mvfst (Meta) | C++ (Folly) | Facebook app, WhatsApp |
| quic-go | Go | Caddy, many Go services |
Kernel QUIC (net/quic/): merged in Linux 6.x mainline discussion ongoing; UDP kernel bypass approach being explored for performance.
QUIC vs TCP+TLS Handshake Comparison
TCP + TLS 1.3 (1-RTT): QUIC (1-RTT first connection):
========================= ==============================
RTT 1 RTT 1
C→S: SYN C→S: Initial[ClientHello]
S→C: SYN-ACK S→C: Initial[ServerHello]
C→S: ACK Handshake[Cert,CertVerify,Finished]
C→S: Handshake[Finished]
RTT 2 1-RTT[HTTP Request]
C→S: ClientHello(key_share) (all in one flight)
S→C: ServerHello(key_share)
+Certificate
+CertVerify
+Finished
RTT 3 (first app data)
C→S: Finished
HTTP Request
TCP + TLS 1.3 (0-RTT resume): QUIC (0-RTT resume):
=========================== ====================
RTT 1 RTT 0 (!!)
C→S: SYN C→S: Initial[ClientHello+PSK]
S→C: SYN-ACK 0-RTT[HTTP Request]
C→S: ACK
ClientHello+PSK RTT 1
+ EarlyData S→C: Initial[ServerHello]
+ HTTP Request Handshake[Finished]
1-RTT[HTTP Response]
RTT 2 (first response)
S→C: ServerHello+Finished
HTTP Response
TCP 0-RTT requires TFO cookie + TLS 0-RTT (2 separate mechanisms).
QUIC 0-RTT is integrated: PSK handles both transport and crypto 0-RTT.
Historical Context
Google began developing QUIC (then called gQUIC or SPDY/QUIC) around 2012 as a way to reduce latency for Chrome browser users. By 2014, a significant fraction of Chrome's HTTPS traffic was using gQUIC. In 2015, Google published details and the IETF formed a working group to standardize it.
IETF QUIC diverged significantly from gQUIC — different packet format, different stream semantics, mandatory TLS 1.3 (gQUIC used a custom crypto layer). RFC 9000 (IETF QUIC) was published in May 2021. HTTP/3 (RFC 9114) defines HTTP semantics over QUIC.
The "QUIC is just encrypted UDP" framing from critics missed the point: QUIC's design allowed Google to iterate on protocol features in Chrome updates — what would take years for TCP (kernel update → OS update → deployment) took weeks for QUIC (Chrome update).
Production Examples
Nginx with QUIC (HTTP/3):
server {
listen 443 quic reuseport; # UDP for QUIC
listen 443 ssl; # TCP for HTTP/2
http3 on;
http3_push off;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
# Tell clients to use HTTP/3 next time
add_header Alt-Svc 'h3=":443"; ma=86400';
}
Check QUIC statistics:
# Using quiche-based NGINX
curl --http3 https://example.com -v 2>&1 | grep 'QUIC\|HTTP/3'
# Cloudflare quiche debug
RUST_LOG=quiche=debug ./quiche-client https://example.com
Debugging Notes
# Check if server supports QUIC
curl --http3-only https://example.com -v 2>&1 | head -5
# Or:
nmap -sU -p 443 --script quic-info example.com
# Wireshark QUIC dissector
# tshark -r capture.pcap -Y 'quic' -T fields -e quic.packet_number
# ssldump alternative: quiche QLOG format
# Most QUIC implementations support --qlog-dir for structured logging
# Test QUIC handshake latency
quiche-client --dump-packets https://example.com 2>&1 | grep 'RTT\|handshake'
# Debug QUIC with curl
curl --http3 -v --resolve 'example.com:443:1.2.3.4' https://example.com
Security Implications
- 0-RTT replay: 0-RTT data must not be used for non-idempotent operations. Servers must implement replay detection (e.g., cache of 0-RTT packet hashes for the PSK's validity window).
- Connection migration hijacking: an attacker can send packets with a captured DCID from a new source. QUIC prevents this with path validation (
PATH_CHALLENGE/RESPONSE), but validation adds 1 RTT. - Amplification attacks: QUIC Initial packets must be at least 1200 bytes. Server responses to Initial packets are limited to 3x the client's Initial packet size until the address is validated. This prevents QUIC from being used as an amplification vector.
- Version negotiation downgrade: QUIC includes a version field. An attacker injecting a
Version Negotiationpacket can attempt to downgrade the client to an older, weaker QUIC version. Modern implementations validate VN packets. - Ossification resistance: the QUIC GREASE mechanism (fixed bit, reserved packet types) intentionally inserts variation to prevent middleboxes from assuming fixed patterns.
Performance Implications
| Metric | TCP + TLS 1.3 | QUIC |
|---|---|---|
| First connection latency | 2 RTT | 1 RTT |
| Resumed connection latency | 2 RTT (or 1 with TFO) | 0 RTT |
| Single-stream throughput | Excellent | Similar |
| Multi-stream with 1% loss | HOL blocking degrades all | Each stream independent |
| CPU overhead (encryption) | AES-NI | AES-NI (same) |
| Server UDP socket overhead | N/A | Higher than TCP (no kernel help) |
QUIC's primary performance advantage is on lossy or high-RTT networks with multiple concurrent streams. On a reliable, low-latency datacenter network with a single stream, QUIC and TCP performance are nearly identical.
QUIC is more CPU-intensive on the server: UDP socket processing lacks TSO/GSO offloads, ACK coalescing (similar to GRO) is done in userspace, and per-datagram overhead is higher than TCP's stream batching.
Failure Modes and Real Incidents
Incident: QUIC UDP port blocking (enterprises) Many enterprise networks block outbound UDP on port 443, causing browsers to spend up to 3 seconds attempting QUIC before falling back to TCP. Chrome's fallback logic was tuned to give up on QUIC faster when initial QUIC connections fail. Cloudflare's data: ~5–8% of internet users have QUIC blocked.
Failure Mode: UDP receive buffer exhaustion
QUIC servers receive all connections on one or a few UDP sockets. At high connection rates, the UDP receive buffer fills between recvmsg() calls. Packets are silently dropped by the kernel. Fix: increase SO_RCVBUF to large values, use SO_REUSEPORT with multiple workers, use recvmmsg() for batch receive.
Incident: 0-RTT replay in POST form submission A CDN mistakenly allowed 0-RTT for all HTTP methods. An attacker captured a 0-RTT POST containing a bank transfer and replayed it 10 times before the bank's deduplicate logic detected it. Industry lesson: 0-RTT must be restricted to safe (GET, HEAD) or explicitly idempotent requests.
Modern Usage
- HTTP/3 adoption (2022–2025): ~30% of web traffic uses HTTP/3/QUIC. Chrome enables QUIC by default. All major CDNs (Cloudflare, Fastly, Akamai, AWS CloudFront) support HTTP/3.
- DNS over QUIC (DoQ, RFC 9250): a DNS transport using QUIC, providing encryption + HOL-blocking-free query multiplexing. Deployed by some public resolvers.
- QUIC for databases: Fauna, CockroachDB, and others are experimenting with QUIC as the wire protocol for intra-cluster replication, taking advantage of 0-RTT and migration.
- MASQUE (Multiplexed Application Substrate over QUIC Encryption): proxying arbitrary UDP/TCP traffic over QUIC tunnels — used for VPNs and proxies that need to traverse middleboxes.
Future Directions
- Multipath QUIC (MP-QUIC, RFC in progress): multiple concurrent paths per QUIC connection — uses all available network interfaces simultaneously for increased throughput and resilience
- Unreliable streams (
DATAGRAMframes, RFC 9221): carry unreliable application data inside a QUIC connection — useful for gaming, real-time media where retransmit is too slow - Kernel QUIC: ongoing Linux kernel implementation to match TCP's TSO/GRO offload performance in the QUIC path
- QUIC for IoT: lightweight QUIC profiles for constrained devices — lower memory footprint, simpler state machine
Exercises
-
Run
curl --http3 https://cloudflare.com -vandcurl --http2 https://cloudflare.com -v. Compare the TTFB (time to first byte). Repeat from a simulated lossy network (tc netem loss 2%) and explain why the QUIC advantage grows with loss. -
Implement a minimal QUIC client using quiche or quic-go that establishes a connection, sends an HTTP/3 GET request, and closes. Instrument the handshake with
QLOGoutput. Analyze the qlog to count the number of packets exchanged during 1-RTT and 0-RTT handshakes. -
Configure nginx-quic on a VM and use Wireshark to capture an HTTP/3 transaction. Identify: Initial, Handshake, and 1-RTT QUIC packets. Count the RTTs required before HTTP response data starts flowing. Compare to an HTTP/2 capture.
-
Simulate HOL blocking: create an HTTP/2 server and an HTTP/3 server. On each, initiate 4 concurrent streams downloading 1MB files. Inject 5% packet loss for one stream only (use tc qdisc with tc filter to target specific flow). Measure completion time for the other 3 streams.
-
Study a QUIC connection migration event: connect via Wi-Fi, transfer a large file, then disconnect Wi-Fi while LTE takes over. Using a QUIC implementation with migration support (quic-go client), observe whether the transfer continues. Capture the PATH_CHALLENGE/RESPONSE exchange with tcpdump.
References
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport
- RFC 9001 — Using TLS to Secure QUIC
- RFC 9002 — QUIC Loss Detection and Congestion Control
- RFC 9114 — HTTP/3
- RFC 9221 — An Unreliable Datagram Extension to QUIC
- Langley, A. et al. The QUIC Transport Protocol: Design and Internet-Scale Deployment. ACM SIGCOMM 2017.
- Iyengar, J. & Thomson, M. QUIC: A UDP-Based Multiplexed and Secure Transport. IETF RFC 9000, 2021.
- quiche source: github.com/cloudflare/quiche
- quic-go source: github.com/quic-go/quic-go
- Cloudflare blog. The Road to QUIC. 2018.
- Google QUIC design documents. chromium.org/quic