skip to content
luminary.blog
by Oz Akan
connected nodes

QUIC Protocol — Deep Dive

A comprehensive deep dive into QUIC, the transport protocol behind HTTP/3 — covering its architecture, handshake, streams, connection migration, congestion control, and future directions.

/ 17 min read

Table of Contents

Origins and Motivation

QUIC (originally “Quick UDP Internet Connections”) was created by Jim Roskind at Google in 2012 and first deployed experimentally in 2013. By 2015, Google submitted a formal proposal to the IETF, and the protocol was standardized as RFC 9000 in May 2021. The IETF version of QUIC diverged significantly from Google’s original “gQUIC” — the two are not interoperable.

QUIC exists because TCP, designed in the 1970s, carries fundamental limitations that can’t be fixed without replacing the transport layer entirely. Three problems drove its creation:

  1. Head-of-line (HoL) blocking at the transport layer. TCP guarantees in-order byte delivery across a single stream 1. When HTTP/2 multiplexes multiple logical streams over one TCP connection, a single lost packet blocks all streams until retransmission completes — even streams whose data arrived fine. This defeats the purpose of multiplexing.

  2. Connection setup latency. A TCP+TLS 1.3 handshake requires 2 round trips (1-RTT TCP + 1-RTT TLS) before any application data can flow. On mobile networks with 100ms+ RTTs, that’s 200–300ms of dead time on every new connection.

  3. Ossification. TCP is so deeply embedded in middleboxes (firewalls, NATs, load balancers) that deploying protocol changes is nearly impossible. Middleboxes inspect and sometimes modify TCP headers, which means any new TCP extension risks being silently dropped or mangled. QUIC encrypts nearly everything above the UDP header, making middlebox interference structurally impossible.

Core Architecture

QUIC is a connection-oriented, encrypted, multiplexed transport protocol built on top of UDP. It integrates transport and cryptographic handshakes into a single operation, making TLS 1.3 a mandatory, inseparable component rather than a layered-on option.

┌─────────────────────────────┐
│ HTTP/3 (RFC 9114) │ Application
├─────────────────────────────┤
│ QUIC (RFC 9000) │ Transport + Encryption
├─────────────────────────────┤
│ UDP │ Datagram
├─────────────────────────────┤
│ IP │ Network
└─────────────────────────────┘

Key architectural decisions:

  • UDP as substrate. QUIC uses UDP purely as a demultiplexing layer to get through NATs and firewalls. All reliability, ordering, congestion control, and flow control are implemented in QUIC itself, in userspace.
  • Encryption by default. Packet headers are authenticated. Payload is encrypted. Even packet numbers are encrypted. Only the connection ID and a few invariant bits are visible to the network. This is a deliberate anti-ossification measure.
  • Userspace implementation. Unlike TCP (implemented in OS kernels), QUIC runs in userspace. This means faster iteration on protocol improvements without waiting for OS updates — but it also means QUIC doesn’t benefit from kernel-level optimizations like TCP segmentation offload.

Connection Setup and Handshake

QUIC’s handshake is one of its most significant improvements. It merges the transport and cryptographic handshakes into a single exchange using TLS 1.3.

1-RTT Handshake (First Connection)

ServerClientServerClientGenerate Initial keysfrom Connection IDDerive Handshake keysDerive Handshake keys,verify server, derive 1-RTT keysConnection established — 1 round tripInitial Packet [CRYPTO: ClientHello + transport params]Initial Packet [CRYPTO: ServerHello]Handshake Packet [CRYPTO: EncryptedExtensions,Certificate, CertificateVerify, Finished]1-RTT Packet [stream data — server can send immediately]Handshake Packet [CRYPTO: Finished]1-RTT Packet [stream data]

The client sends a ClientHello in its very first packet. The server responds with its ServerHello, certificate, and can immediately begin sending encrypted application data — all in a single round trip. Compare this to TCP+TLS 1.3 which requires a TCP SYN/SYN-ACK before TLS even begins.

0-RTT Resumption

When a client has previously connected to a server, it can cache a Pre-Shared Key (PSK) from that session. On reconnection, the client sends application data alongside its ClientHello — zero round trips before data starts flowing.

ServerClientServerClientHas cached PSKfrom previous sessionValidate PSK,derive 0-RTT keys,process early dataFull connection establishedInitial [ClientHello + early data (0-RTT)]Initial [ServerHello]Handshake [Finished + new session ticket]1-RTT [response data]Handshake [Finished]

0-RTT security tradeoff: 0-RTT data is not forward-secret (it uses the PSK from a previous session) and is vulnerable to replay attacks — an attacker could capture and re-send the 0-RTT data. Servers must implement replay protection (e.g., single-use tokens, strike registers) or restrict 0-RTT to idempotent requests. This is why 0-RTT is typically used for safe operations like GET requests, not mutations.

Encryption Levels

QUIC uses four distinct encryption levels during connection establishment, each with its own keys:

LevelKeys Derived FromUsed For
InitialConnection ID (deterministic)ClientHello, ServerHello — effectively cleartext to anyone who sees the connection ID
0-RTTPre-Shared Key (previous session)Early application data before handshake completes
HandshakeTLS handshake exchangeCertificate exchange, handshake completion
1-RTT (Application)Full TLS 1.3 derivationAll post-handshake application data — full forward secrecy

Each encryption level has its own packet number space, preventing cross-level interference.

Packet Format

QUIC defines two header forms: long headers used during connection establishment and short headers used for post-handshake data transfer.

Long Header (Connection Establishment)

Long Header Packet {
Header Form (1 bit) = 1
Fixed Bit (1 bit) = 1
Long Packet Type (2 bits) [Initial, 0-RTT, Handshake, Retry]
Type-Specific Bits (4 bits)
Version (32 bits)
DCID Length (8 bits)
Destination Connection ID (0-160 bits)
SCID Length (8 bits)
Source Connection ID (0-160 bits)
Type-Specific Payload
}

Long header packets are used for Initial, 0-RTT, Handshake, and Retry packet types. They carry both source and destination connection IDs because the connection ID mapping hasn’t been established yet.

Short Header (Post-Handshake)

Short Header Packet (1-RTT) {
Header Form (1 bit) = 0
Fixed Bit (1 bit) = 1
Spin Bit (1 bit) [latency measurement]
Reserved (2 bits)
Key Phase (1 bit) [key rotation]
Packet Number Length (2 bits)
Destination Connection ID (variable)
Packet Number (8-32 bits) [encrypted]
Payload (encrypted)
}

Short header packets are used for all post-handshake data. They carry only the destination connection ID (saving bytes), and the packet number is encrypted to prevent tracking and ossification.

Frames

Inside each QUIC packet payload (after decryption), you find one or more frames. Frames are the actual units of protocol communication:

Frame TypePurpose
STREAMCarries application data for a specific stream
ACKAcknowledges received packets
CRYPTOCarries TLS handshake messages
NEW_CONNECTION_IDProvides new connection IDs for migration
RETIRE_CONNECTION_IDRetires old connection IDs
MAX_STREAM_DATAFlow control: per-stream data limit
MAX_DATAFlow control: connection-level data limit
STREAM_DATA_BLOCKEDSignals flow control limit reached
RESET_STREAMAbruptly terminates a stream
STOP_SENDINGRequests peer stop sending on a stream
PINGKeep-alive / path validation
CONNECTION_CLOSETerminates the connection
PADDINGPadding for packet size requirements
NEW_TOKENProvides address validation token for future 0-RTT
PATH_CHALLENGE / PATH_RESPONSEPath validation during migration

Multiple frames of different types can be packed into a single packet. A single packet might contain a STREAM frame for one stream, an ACK frame, and a MAX_DATA frame — all in one UDP datagram.

Streams and Multiplexing

Streams are QUIC’s solution to head-of-line blocking. Each stream is an independent, ordered sequence of bytes. Loss on one stream does not block delivery on any other stream.

Stream Types

QUIC identifies streams by a 62-bit stream ID. The two least significant bits encode the stream type:

BitsInitiated ByDirection
0x00ClientBidirectional
0x01ServerBidirectional
0x02ClientUnidirectional
0x03ServerUnidirectional

Either endpoint can open streams without negotiation (up to flow control limits). Streams are cheap — you can open thousands of them. In HTTP/3, each request-response pair uses its own stream.

How HoL Blocking Is Eliminated

HTTP/3 over QUIC

Stream 1

Packet Lost

Only Stream 1 blocked

Stream 2

Delivered normally

Stream 3

Delivered normally

HTTP/2 over TCP

TCP Byte Stream

Packet Lost

ALL streams blocked

waiting for retransmission

In TCP, a lost packet means the kernel buffers all subsequent data (even for other HTTP/2 streams) until the missing segment is retransmitted and acknowledged. In QUIC, loss recovery is per-stream. If a packet carrying Stream 4 data is lost, Streams 1, 2, and 3 continue delivering data to the application without any delay.

This difference is most impactful on lossy networks (mobile, WiFi, congested links). On a clean, low-loss fiber connection, HoL blocking rarely triggers, so the benefit is marginal.

Flow Control

QUIC implements flow control at two levels:

  • Stream-level: Each stream has a maximum amount of data the receiver is willing to buffer (advertised via MAX_STREAM_DATA frames).
  • Connection-level: A global limit across all streams on the connection (advertised via MAX_DATA frames).

This prevents any single stream from consuming all available buffer space and starving other streams.

Connection IDs and Migration

Every QUIC connection is identified by connection IDs rather than the traditional TCP 4-tuple (source IP, source port, destination IP, destination port). This is what enables connection migration.

How Connection Migration Works

ServerClient (Phone)ServerClient (Phone)Connected via WiFiIP: 192.168.1.50Moves to cellularIP: 10.0.0.42Same CID → same connection.Validate new path.Path validated.Connection continues seamlessly.Packets with CID: abc123Packets with CID: xyz789Packets with CID: abc123 (new source IP)PATH_CHALLENGEPATH_RESPONSENEW_CONNECTION_ID (rotate for privacy)

When a device switches networks (WiFi to cellular, or between WiFi access points), the IP address changes. TCP connections die because the 4-tuple changes. QUIC connections survive because the server identifies the connection by its connection ID, not the network address.

Connection ID Rotation

To prevent network observers from linking the same connection across different networks (which would enable tracking a user’s physical movements), QUIC supports connection ID rotation. Each endpoint can supply the other with a pool of connection IDs via NEW_CONNECTION_ID frames. When migrating, the client uses a fresh connection ID, making the connection unlinkable across networks from an observer’s perspective.

Path Validation

After migration, the server sends a PATH_CHALLENGE frame to the new address. The client must respond with the correct PATH_RESPONSE. This prevents an attacker from redirecting a connection to a victim’s IP address (traffic amplification attack).

Congestion Control and Loss Recovery

QUIC specifies its own congestion control and loss recovery mechanisms in RFC 9002. The protocol is designed to be congestion control algorithm agnostic — implementations can use any algorithm as long as they are “network-friendly” (don’t cause more congestion than TCP would).

Default Algorithm

RFC 9002 specifies a NewReno-like algorithm as the default, but major implementations use more sophisticated approaches:

ImplementationDefault Algorithm
Google (Chromium)BBR v2
Cloudflare (quiche)CUBIC
Facebook (mvfst)BBR / CCP
AppleCUBIC
Microsoft (MsQuic)CUBIC

Loss Detection

QUIC uses two mechanisms to detect packet loss:

  1. Packet threshold: A packet is declared lost when a later packet in the same packet number space has been acknowledged and the gap exceeds 3 packets (the kPacketThreshold). This is analogous to TCP’s triple duplicate ACK.

  2. Time threshold: A packet is declared lost if enough time has passed since the packet was sent and a later packet has been acknowledged. The default time threshold is 9/8 of the maximum RTT observed (kTimeThreshold = 9/8). This catches losses in sparse traffic where the packet threshold wouldn’t trigger.

Key Differences from TCP Loss Recovery

  • No ambiguous retransmissions. TCP reuses sequence numbers for retransmissions, making it impossible to tell if an ACK is for the original or the retransmit. QUIC uses monotonically increasing packet numbers — every packet has a unique number, even retransmissions. The retransmitted data gets a new packet number, so ACKs are unambiguous. This gives QUIC more accurate RTT measurements.

  • ACK frames are more expressive. QUIC ACKs can report multiple ranges of received packets (similar to TCP SACK), and they’re a standard part of the protocol rather than an optional extension.

  • Explicit Congestion Notification (ECN). QUIC has built-in support for ECN, allowing routers to signal congestion without dropping packets. ECN feedback is carried in ACK frames.

Probe Timeout (PTO)

QUIC replaces TCP’s retransmission timeout (RTO) with a Probe Timeout (PTO) mechanism. When the PTO fires, the sender transmits a probe packet (usually new data, or an ACK-eliciting frame) rather than aggressively retransmitting potentially lost data. This avoids the head-of-line blocking that TCP’s RTO causes when it retransmits and backs off exponentially.

HTTP/3: QUIC’s Primary Application Protocol

HTTP/3 (RFC 9114) is HTTP mapped onto QUIC. It replaces HTTP/2’s binary framing layer with QUIC streams, eliminating the need for HTTP/2’s complex multiplexing machinery.

How HTTP/3 Maps to QUIC

HTTP/3 over QUIC

Request 1

QUIC Stream 0

Request 2

QUIC Stream 4

Request 3

QUIC Stream 8

Control

QUIC Stream 2

QPACK Encoder

QUIC Stream 6

HTTP/2 over TCP

HTTP/2 Framing Layer

Single TCP Stream

Multiplexing in HTTP layer

Each HTTP request-response pair maps to a dedicated QUIC stream. No more HTTP-level multiplexing needed — QUIC handles it natively at the transport layer.

QPACK: Header Compression

HTTP/2 uses HPACK for header compression, which relies on a synchronized, in-order compression state between endpoints. This works fine over TCP’s ordered byte stream but would reintroduce head-of-line blocking over QUIC’s independent streams.

HTTP/3 uses QPACK (RFC 9204) instead. QPACK uses dedicated unidirectional streams for encoder and decoder instructions, separating the compression state updates from the request/response streams. This allows header compression without requiring in-order delivery across streams.

Key HTTP/3 Stream Types

StreamDirectionPurpose
Control streamUni, one per endpointConnection-level settings (SETTINGS, GOAWAY)
QPACK encoder streamUni, one per endpointDynamic table updates
QPACK decoder streamUni, one per endpointTable update acknowledgments
Request streamsBidi, client-initiatedIndividual HTTP request-response pairs
Push streamsUni, server-initiatedServer push (rarely used)

Performance Characteristics

Where QUIC Wins

High-latency networks (mobile, satellite): The 1-RTT (or 0-RTT) handshake saves 1–2 round trips per new connection. On a 150ms RTT mobile connection, that’s 150–300ms saved before any data flows. This is the single largest performance win for most users.

Lossy networks (WiFi, congested links): Independent stream delivery means a 2% packet loss rate doesn’t stall all in-flight requests. Tests by Google and Cloudflare show QUIC delivering 5–15% better page load times on lossy connections compared to HTTP/2 over TCP.

Connection migration (mobile users): Users moving between WiFi and cellular no longer experience connection drops. The transition is seamless — in-flight requests continue without re-establishment.

Multiplexed workloads: Pages loading many small resources in parallel benefit the most from HoL blocking elimination.

Where QUIC Is Comparable or Slower

Clean, low-latency networks: On a stable fiber connection with <1% packet loss, the HoL blocking benefit rarely activates. TCP+TLS 1.3 already achieves 1-RTT handshakes for new connections and 0-RTT for resumption. The performance difference is negligible.

High-bandwidth bulk transfers: QUIC’s userspace implementation means it uses more CPU per byte than kernel-optimized TCP with hardware offloading (TSO, GRO, checksum offload). On server-side benchmarks, QUIC can use 2–3x more CPU than TCP for the same throughput. This gap is closing as implementations mature and leverage techniques like UDP GSO (Generic Segmentation Offload).

Environments that block UDP: Some corporate networks and ISPs throttle or block UDP traffic on non-standard ports. QUIC typically runs on UDP port 443, but still faces occasional blocking. Implementations fall back to TCP+TLS when QUIC fails.

Benchmark Summary

ScenarioQUIC vs TCP+TLSReason
First connection, high RTT30–40% faster1-RTT vs 2-RTT handshake
Repeated connection, high RTTUp to 50% faster0-RTT resumption
2% packet loss, multiplexed5–15% fasterNo cross-stream HoL blocking
Clean fiber, bulk transferComparable or slightly slowerCPU overhead in userspace
Network migration (WiFi→cell)Seamless vs connection resetConnection IDs survive IP change

Security Properties

QUIC’s security model is substantially stronger than TCP+TLS:

  • Always encrypted. There is no unencrypted QUIC. TLS 1.3 is mandatory and integrated, not optional.
  • Header protection. Packet numbers and other header fields are encrypted, preventing ossification and tracking.
  • Authenticated headers. Even the unencrypted invariant header bits are authenticated, preventing tampering.
  • Amplification protection. Servers are limited to sending 3x the data received from an unverified client address, preventing QUIC from being used in amplification DDoS attacks.
  • Forward secrecy. All 1-RTT data has full forward secrecy through ephemeral Diffie-Hellman key exchange. (0-RTT data does not have forward secrecy — this is an inherent tradeoff.)

Retry Mechanism

To validate a client’s address before committing server resources, the server can respond to the initial ClientHello with a Retry packet containing an opaque token. The client must resubmit the Initial packet with this token, proving it can receive traffic at its claimed address. This adds one round trip but prevents address spoofing attacks.

Congestion Control Deep Dive: BBR vs CUBIC

Since most major QUIC deployments use either BBR or CUBIC, understanding the difference matters:

CUBIC

CUBIC is a loss-based congestion control algorithm. It increases the sending rate following a cubic function of time since the last loss event. When packet loss occurs, it reduces the congestion window and slowly recovers.

  • Strength: Well-understood, fair with other TCP flows, predictable behavior.
  • Weakness: On networks with large buffers (bufferbloat), CUBIC fills the buffers before detecting loss, causing high latency. On lossy wireless links, it misinterprets random loss as congestion.

BBR (Bottleneck Bandwidth and RTT)

BBR, developed by Google, is a model-based algorithm. Instead of reacting to loss, it actively measures the bottleneck bandwidth and minimum RTT of the path, then paces packets to match the estimated bottleneck rate.

  • Strength: Achieves high throughput without filling network buffers. Performs well on long-distance, high-bandwidth paths where CUBIC underutilizes capacity.
  • Weakness: BBRv1 had fairness issues — it could dominate CUBIC flows sharing the same bottleneck. BBRv2 addresses many of these concerns with improved loss response and fairness mechanisms.

Future Directions

Multipath QUIC

RFC 9443 (published 2024) defines the Multipath extension for QUIC. It allows a single QUIC connection to send and receive data across multiple network paths simultaneously (e.g., WiFi and cellular at the same time).

Unlike basic connection migration (which switches from one path to another), Multipath QUIC uses both paths concurrently for:

  • Bandwidth aggregation: Combine the throughput of multiple interfaces.
  • Seamless failover: If one path degrades, traffic shifts to the other without interruption.
  • Latency optimization: Duplicate critical data across paths for the lowest latency delivery.

Each path has its own congestion controller, preventing one path’s congestion from affecting the other.

MASQUE (Multiplexed Application Substrate over QUIC Encryption)

MASQUE (RFC 9298, RFC 9484) uses QUIC’s stream multiplexing to build proxy and tunneling protocols. Key applications:

  • CONNECT-UDP: Proxy UDP traffic over a QUIC connection. Enables VPN-like functionality without kernel-level tunneling.
  • CONNECT-IP: Proxy IP-level traffic. Can replace traditional VPN protocols.
  • iCloud Private Relay and Google One VPN both use MASQUE as their underlying transport.

MASQUE is significant because it builds on QUIC’s properties (encryption, multiplexing, migration) to create proxy protocols that are faster and more privacy-preserving than traditional VPN tunnels.

DNS over QUIC (DoQ)

RFC 9250 defines DNS over QUIC. Benefits over DNS over HTTPS (DoH) and DNS over TLS (DoT):

  • Lower latency: QUIC’s 1-RTT handshake means DNS queries start faster than DoT’s TCP+TLS setup.
  • 0-RTT queries: Resumed connections can send DNS queries with zero handshake latency.
  • Independent streams: Multiple DNS queries don’t block each other (unlike DoT where a single TCP connection creates HoL blocking for DNS responses).
  • Connection migration: DNS resolution survives network changes.

WebTransport

WebTransport (W3C spec, uses HTTP/3) gives web applications direct access to QUIC-like capabilities: unreliable datagrams, multiple independent streams, and low-latency bidirectional communication. It’s positioned as a more capable alternative to WebSockets for use cases like real-time gaming, live media, and IoT dashboards.

Implementation Landscape

LibraryLanguageUsed ByNotes
quicheRustCloudflare, curlProduction-grade, well-documented
MsQuicCWindows, .NETMicrosoft’s cross-platform implementation
ngtcp2Ccurl (alternative)Minimal, embeddable
quic-goGoCaddy, TraefikPopular in Go ecosystem
aioquicPythonTesting, prototypingasyncio-based
s2n-quicRustAWSAmazon’s production implementation
mvfstC++MetaFacebook’s internal QUIC stack
Chromium QUICC++Chrome, AndroidGoogle’s reference implementation
Apple QUICSwift/CiOS, macOSBuilt into Apple’s networking stack

QUIC vs TCP+TLS — Summary Comparison

PropertyTCP + TLS 1.3QUIC
TransportKernel TCPUserspace over UDP
EncryptionOptional (TLS layered on top)Mandatory (TLS 1.3 integrated)
Handshake (new)2 RTT (1 TCP + 1 TLS)1 RTT
Handshake (resumed)1 RTT (TCP) + 0-RTT (TLS)0 RTT
MultiplexingHTTP/2 in application layerNative streams in transport
HoL blockingYes (across all streams)No (per-stream only)
Connection migrationNot supportedSupported via connection IDs
Header encryptionNo (plaintext TCP headers)Yes (packet numbers encrypted)
Middlebox visibilityFullMinimal (by design)
CPU overheadLow (kernel + hardware offload)Higher (userspace, improving)
Ecosystem maturityDecades of optimizationRapidly maturing
FallbackN/AFalls back to TCP+TLS

RFCs and Standards Reference

RFCTitlePublished
RFC 9000QUIC: A UDP-Based Multiplexed and Secure TransportMay 2021
RFC 9001Using TLS to Secure QUICMay 2021
RFC 9002QUIC Loss Detection and Congestion ControlMay 2021
RFC 9114HTTP/3June 2022
RFC 9204QPACK: Field Compression for HTTP/3June 2022
RFC 9221An Unreliable Datagram Extension to QUICMarch 2022
RFC 9250DNS over Dedicated QUIC ConnectionsMay 2022
RFC 9298Proxying UDP in HTTP (CONNECT-UDP)August 2022
RFC 9369QUIC Version 2May 2023
RFC 9443Multipath Extension for QUICJuly 2024
RFC 9484Proxying IP in HTTP (CONNECT-IP)October 2023

Footnotes

  1. TCP treats everything it sends as a single, continuous sequence of bytes — each byte gets a sequence number. The receiving side must deliver those bytes to the application in exactly the order they were sent. If bytes 1–1000 and 2001–3000 arrive but bytes 1001–2000 are lost in transit, the kernel buffers the later data and waits. The application sees nothing new until the missing segment is retransmitted and fills the gap. This is the “in-order delivery guarantee” — it’s reliable, but it means one lost packet holds up everything behind it, even data that’s already sitting in the receive buffer ready to go.