Skip to content

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 Negotiation packet 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 (DATAGRAM frames, 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

  1. Run curl --http3 https://cloudflare.com -v and curl --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.

  2. 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 QLOG output. Analyze the qlog to count the number of packets exchanged during 1-RTT and 0-RTT handshakes.

  3. 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.

  4. 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.

  5. 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