How QUIC Actually Works: The Transport Protocol That Moved TCP Into User Space
Try the interactive lab for this articleTake the quiz (6 questions · ~5 min)Most transport protocols are invisible until they become the bottleneck. TCP spent decades doing that job well enough that most applications never had to think about it. Then the web changed. Browsers started opening dozens of objects per page, mobile clients started roaming between networks, and latency became dominated by handshake costs, head-of-line blocking, and the inertia of kernel-level transport code that ships on operating system schedules. QUIC exists because fixing those problems inside TCP had become politically, operationally, and technically painful.
QUIC is often described as "TCP over UDP," which is not wrong, but it is much too shallow to be useful. QUIC is a secure transport protocol that runs in user space, uses UDP only as a carriage layer, integrates TLS 1.3 directly into the transport handshake, multiplexes streams without transport-level head-of-line blocking, supports connection migration, and ships protocol evolution on application release cycles instead of operating system release cycles. That is a major change in how internet transport gets built and deployed.
This post walks through how QUIC actually works: why HTTP/3 needed it, how the handshake is structured, how packets and packet number spaces are managed, how streams are multiplexed, how loss recovery differs from TCP, and what operators and developers feel in the real world.
1. Why QUIC Exists At All
HTTP/2 solved one visible problem on the web, which was HTTP/1.1 connection explosion. A browser no longer needed six parallel TCP connections per origin just to avoid application-layer head-of-line blocking. It could open one TCP connection and multiplex multiple request-response streams over it.
That helped, but it also exposed a deeper problem. HTTP/2 multiplexing happens above TCP. If one TCP segment is lost, every HTTP/2 stream behind that missing byte stalls until the kernel retransmits and the receiver gets the missing data in order. This is transport-level head-of-line blocking.
Suppose a browser in Athens opens one HTTP/2 connection to fetch:
- HTML
- CSS
- JavaScript bundle
- fonts
- five image tiles
If a single TCP segment carrying part of image tile 4 is lost, the missing bytes create a hole in the TCP byte stream. The receiver cannot deliver bytes beyond that hole to the HTTP/2 layer, even if those later bytes belong to CSS or JavaScript that arrived intact. TCP only exposes an ordered byte stream. It does not know anything about streams inside that byte stream.
That problem could not be solved cleanly inside HTTP/2 because the blockage is below HTTP/2. It also could not be solved cleanly inside TCP because:
- TCP is ossified by decades of middleboxes
- adding new TCP options is operationally difficult
- transport changes require kernel updates
- encrypted metadata and integrated crypto do not fit naturally into classic TCP
QUIC takes a different approach. Keep IP for routing. Keep UDP as the thin demultiplexing layer that middleboxes already tolerate. Move the real transport logic into user space. Ship congestion control, loss recovery, stream scheduling, and handshake logic with the application.
QUIC is more than a performance optimization. It is a deployment strategy.
2. QUIC Is Not "Just UDP"
At the socket API level, QUIC packets are UDP datagrams. At the protocol level, QUIC behaves like a full transport:
- connection establishment
- reliability
- congestion control
- loss detection
- flow control
- stream multiplexing
- path validation
- cryptographic identity
UDP contributes almost none of that. UDP gives QUIC:
- source port
- destination port
- length
- checksum
That is effectively it.
The reason QUIC uses UDP is not because UDP is sophisticated. It is because UDP is simple enough to pass through existing network equipment without inheriting TCP's semantics and option space.
This design has a consequence that surprises people the first time they inspect it closely. QUIC has to rebuild large parts of what TCP used to provide in the kernel:
- packet numbering
- acknowledgments
- retransmission logic
- congestion window management
- receive reordering
- path MTU handling
It does that while also encrypting almost everything beyond the small amount of header information needed for routing the packet to the correct connection.
3. QUIC Packet Structure: Long Headers, Short Headers, and Frames
Before looking at the handshake, it helps to understand what QUIC packets look like on the wire. QUIC defines two header formats: the long header and the short header. They serve different purposes at different stages of the connection lifecycle.
Long Header Format
Long headers are used during connection establishment and for special packet types. Every long header packet starts with a single byte whose high bit is set to 1, followed by a fixed-form bit set to 1, and then two bits indicating the packet type.
The structure looks like this:
Long Header Packet {
Header Form (1 bit) = 1,
Fixed Bit (1 bit) = 1,
Long Packet Type (2 bits),
Type-Specific Bits (4 bits),
Version (32 bits),
Destination Connection ID Length (8 bits),
Destination Connection ID (0..160 bits),
Source Connection ID Length (8 bits),
Source Connection ID (0..160 bits),
Type-Specific Payload (..)
}Long header packet types include:
| Type Value | Packet Type |
|---|---|
| 0x00 | Initial |
| 0x01 | 0-RTT |
| 0x02 | Handshake |
| 0x03 | Retry |
The Initial packet is what the client sends first. It must be at least 1200 bytes (padded if necessary), which ensures the path supports a reasonable MTU and helps with the anti-amplification budget. The Retry packet is special: the server sends it to force the client to prove it can receive packets at its claimed source address, using a retry token. This is one of QUIC's defences against address spoofing during the handshake.
Short Header Format
Once the handshake completes and both sides have 1-RTT keys, QUIC switches to the short header. It is more compact because many fields are no longer needed:
Short Header Packet {
Header Form (1 bit) = 0,
Fixed Bit (1 bit) = 1,
Spin Bit (1 bit),
Reserved Bits (2 bits),
Key Phase (1 bit),
Packet Number Length (2 bits),
Destination Connection ID (variable),
Packet Number (8..32 bits),
Packet Payload (..)
}The short header does not carry a Source Connection ID or a version field. Those are only needed during setup. The spin bit is a clever addition: it alternates between 0 and 1 once per round trip, giving passive observers a way to estimate RTT without decrypting anything. Network operators who lost most of their TCP visibility (because QUIC encrypts almost everything) can still get RTT measurements from the spin bit, though the precision is limited.
The key phase bit allows endpoints to rotate encryption keys mid-connection without any interruption to data flow. When one side begins using a new key, it flips this bit so the peer knows which key to use for decryption.
Frames Inside Packets
A QUIC packet payload contains one or more frames. Frames are the real unit of work. The packet is just the encryption and delivery envelope. Common frame types include:
| Frame Type | Purpose |
|---|---|
| PADDING | Increase packet size (used for Initial padding, PMTU probing) |
| PING | Keep connection alive, elicit ACK |
| ACK | Acknowledge received packets |
| CRYPTO | Carry TLS handshake data |
| STREAM | Carry application data for a stream |
| MAX_DATA | Update connection-level flow control |
| MAX_STREAM_DATA | Update per-stream flow control |
| MAX_STREAMS | Allow peer to open more streams |
| DATA_BLOCKED | Signal that flow control is limiting the sender |
| NEW_CONNECTION_ID | Provide new CIDs for migration |
| RETIRE_CONNECTION_ID | Request retirement of old CIDs |
| PATH_CHALLENGE | Validate a new network path |
| PATH_RESPONSE | Respond to path validation |
| CONNECTION_CLOSE | Terminate the connection |
| HANDSHAKE_DONE | Server signals handshake confirmation |
Multiple frames can be packed into a single packet, and frames from different streams can share the same packet. This is how QUIC achieves efficient multiplexing at the wire level: a single UDP datagram might carry an ACK frame, a STREAM frame for stream 4, and another STREAM frame for stream 8, all in one packet.
A packet carrying only ACK frames is not ack-eliciting, meaning the peer does not need to acknowledge it. This prevents acknowledgment loops where both sides keep sending ACKs to acknowledge previous ACKs.
4. The QUIC Handshake: Transport and TLS Combined
Classic HTTPS over TCP requires at least two layers of establishment:
- TCP three-way handshake
- TLS handshake on top of TCP
Even with TLS 1.3 reducing cryptographic setup to one round trip, you still pay the TCP handshake first. QUIC removes that split. It carries the TLS 1.3 handshake inside QUIC frames. Transport and cryptographic establishment happen together.
The simplest first-connection flow looks like this:
Client Server
Initial packet
CRYPTO(ClientHello)
------------------------>
Initial packet
CRYPTO(ServerHello...)
Handshake packet
CRYPTO(EncryptedExtensions...)
1-RTT keys become derivable
<------------------------
Initial/Handshake packet
CRYPTO(Finished)
STREAM(request)
------------------------>From the application's point of view, the connection is usable after one round trip.
Why the Handshake Looks Strange
QUIC uses TLS 1.3, but TLS is not allowed to send records directly on the wire. Instead, TLS handshake bytes are carried inside QUIC CRYPTO frames. QUIC handles packetization, retransmission, ACKs, and encryption levels. TLS handles certificate validation, key schedule, and handshake transcript semantics.
This split is elegant once you see it clearly:
- TLS decides what cryptographic bytes must be exchanged
- QUIC decides how those bytes move, are retransmitted, and become protected at each encryption level
The Crypto Handshake Step by Step
The 1-RTT handshake proceeds through these stages:
-
The client generates an ephemeral key pair (typically X25519 or P-256) and assembles a TLS ClientHello containing supported cipher suites, key shares, and transport parameters encoded as a TLS extension.
-
The client places the ClientHello into a CRYPTO frame inside an Initial packet. This packet is encrypted with Initial keys derived from a publicly known salt and the server's Destination Connection ID. The Initial packet must be padded to at least 1200 bytes.
-
The server receives the Initial packet, derives the same Initial keys, decrypts, and processes the ClientHello. It selects a cipher suite and key share, computes the handshake traffic secret using HKDF, and assembles its ServerHello.
-
The server sends its ServerHello in an Initial packet (using Initial encryption) and then sends EncryptedExtensions, Certificate, CertificateVerify, and Finished in Handshake packets (using Handshake encryption derived from the handshake traffic secret). The server also includes its QUIC transport parameters in the EncryptedExtensions message.
-
The client processes the server's flight, validates the certificate chain, verifies the CertificateVerify signature, and derives 1-RTT keys. It sends its own Finished message and can immediately begin sending application data using 1-RTT encryption.
-
The server receives the client's Finished, confirms the handshake, and sends a HANDSHAKE_DONE frame to signal that the handshake is confirmed from the server's perspective.
QUIC transport parameters exchanged during this handshake include values like initial_max_data, initial_max_stream_data_bidi_local, initial_max_streams_bidi, max_idle_timeout, and active_connection_id_limit. These replace what TCP would negotiate through SYN options, but with a much richer set of knobs.
Encryption Levels
QUIC traffic exists in multiple encryption levels during setup:
- Initial
- Handshake
- 0-RTT, optional
- 1-RTT
Initial keys are derived from a public salt and the client's Destination Connection ID, so they are not secret in the long-term sense. They simply protect the early handshake from trivial interference and give the protocol structured packet handling from the first datagram. Handshake keys and then 1-RTT keys take over as TLS progresses.
This staged protection is one of the reasons QUIC packet traces look more complex than TCP packet traces. The protocol is changing protection level while the connection is still being established.
5. Connection IDs: Why QUIC Connections Survive Path Changes
TCP identifies a connection by a 4-tuple:
- source IP
- source port
- destination IP
- destination port
That works until one of those changes. On mobile devices, they change all the time. A phone can move from home Wi-Fi to 5G while the application still wants the same logical session. A NAT can rebind the source port. A multi-homed server can reply from a different interface. TCP interprets these as a different flow. QUIC does not have to.
QUIC introduces Connection IDs, or CIDs. Endpoints can assign opaque identifiers to the connection so that packets can still be routed to the right session even if the 4-tuple changes.
A simplified packet-routing model looks like this:
UDP datagram arrives
-> parse QUIC header
-> extract Destination Connection ID
-> look up connection state by CID
-> hand packet to that QUIC connectionThis means the server does not have to key connection state solely by client IP and port. If the client moves from a flat in Barcelona on Wi-Fi to 5G while walking outside, the next packet can still map to the same connection state if the CID remains valid and the path is validated.
Why Path Validation Exists
Connection migration would be an amplification attack gift if the server blindly trusted every new source address. An attacker could spoof packets with someone else's IP and trick the server into flooding that victim. QUIC defends against this with path validation.
When an endpoint observes a new path, it probes it with frames such as:
PATH_CHALLENGEPATH_RESPONSE
Only after the peer proves reachability on that path should large amounts of traffic move over it.
This is a major operational difference from TCP. QUIC connections can survive network changes, but they do not do so naively.
Anti-Amplification Limit
The server also has to respect QUIC's anti-amplification rule before the client's address is validated. In simplified form, if the server has only received N bytes from an unvalidated address, it should not send more than about 3N bytes back.
That rule exists because QUIC runs over UDP. Without it, an attacker could spoof a victim's source IP, send a tiny Initial packet, and trick the server into sending a much larger handshake flight at the victim. The anti-amplification budget limits how useful that attack can be.
In practice this means certificate chains matter operationally. A very large certificate chain eats the server's early send budget quickly. If the first client flight is small and the server's handshake flight is large, the server may need another client packet before it can finish sending everything.
6. Packet Numbers and Packet Number Spaces
TCP sequence numbers represent byte positions in a stream. QUIC packet numbers represent sent packets in a packet number space. Those are different ideas and they solve different problems.
Each QUIC packet sent in a given packet number space gets a strictly increasing packet number. The sender never reuses one. If a packet's contents need retransmission, QUIC sends the relevant frames again in a new packet with a new packet number.
That is important. TCP retransmits a lost segment using the same sequence numbers because sequence numbers are tied to byte positions. QUIC does not do "packet retransmission" in that exact sense. It performs frame retransmission.
Why That Matters
Loss detection becomes cleaner because acknowledgments refer to packet numbers that were actually observed on the wire. There is no ambiguity between:
- original transmission
- retransmission
- spurious retransmission
If packet 120 is lost and packet 133 carries the replacement data, ACK processing can reason about those events separately.
TCP suffers from the retransmission ambiguity problem: when a retransmitted segment is acknowledged, the sender cannot be certain whether the ACK covers the original transmission or the retransmission. This ambiguity contaminates RTT estimation (the Karn/Partridge problem) and complicates loss detection logic. QUIC avoids this entirely because every packet gets a unique number. If packet 133 is acknowledged, the sender knows exactly when packet 133 was sent and can compute the RTT sample without guesswork.
Separate Packet Number Spaces
QUIC uses separate packet number spaces for:
- Initial
- Handshake
- Application data, which includes 0-RTT and 1-RTT
That avoids weird coupling between handshake loss and application loss. ACKs for Initial packets only acknowledge Initial packets. Handshake packet loss tracking is separate from 1-RTT packet loss tracking.
A simplified view:
Initial space: 0, 1, 2, 3
Handshake space: 0, 1, 2
1-RTT space: 0, 1, 2, 3, 4, 5The same numeric value can exist in multiple spaces without conflict because the encryption level tells the receiver which space it belongs to.
Packet Number Encoding
Packet numbers on the wire are not encoded as full 64-bit values. QUIC uses a variable-length encoding (1, 2, or 4 bytes) that transmits only the least significant bits. The receiver reconstructs the full packet number using the largest acknowledged packet number as a reference point. This works because packet numbers are strictly increasing: the receiver knows the full number must be close to the most recent one, so it can infer the high bits.
For example, if the largest acknowledged packet number is 0x1234 and the sender encodes the next packet number as a 2-byte value 0x1235, the receiver knows the full number without needing all 64 bits on the wire. This saves header space on every single packet, which adds up across millions of packets.
7. QUIC Streams: Ordered Delivery Where It Matters, Not Everywhere
QUIC provides a connection made of many independent streams. Each stream has its own ordered byte space. Loss on one stream does not block delivery on another stream.
This is the part that directly addresses HTTP/2 over TCP pain.
Imagine three streams on one connection:
- stream 0: HTML
- stream 4: CSS
- stream 8: image tile
If a packet carrying part of stream 8 is lost, bytes for streams 0 and 4 can still be delivered as long as their own per-stream ordering is intact. The connection does not stall behind an unrelated missing byte from another stream.
Stream Types
QUIC defines:
- bidirectional streams
- unidirectional streams
Clients and servers can create streams from both categories, with stream IDs encoding:
- initiator, client or server
- directionality, uni or bidi
The low bits of the stream ID carry those properties. For example:
| Stream ID | Initiator | Type |
|---|---|---|
| 0 | client | bidi |
| 1 | server | bidi |
| 2 | client | uni |
| 3 | server | uni |
| 4 | client | bidi |
HTTP/3 uses these streams for specific roles:
- request and response bodies on request streams
- control stream
- QPACK encoder stream
- QPACK decoder stream
Stream Lifecycle and States
A stream goes through a well-defined lifecycle. For a sending stream, the states are:
- Ready: the stream exists but no data has been sent yet.
- Send: data is being transmitted. The sender can send STREAM frames and must respect flow control limits.
- Data Sent: all data has been sent (including the FIN flag), but not all has been acknowledged yet.
- Data Recvd: all data has been acknowledged. Terminal state.
- Reset Sent: the sender aborted the stream with a RESET_STREAM frame.
For a receiving stream, the mirror states are:
- Recv: accepting data from the peer.
- Size Known: received the FIN, so the total size is known, but not all bytes have arrived.
- Data Recvd: all bytes received. Ready to deliver to the application.
- Data Read: the application has consumed all bytes. Terminal state.
- Reset Recvd: the peer aborted the stream.
Either side can abort a stream without affecting other streams on the connection. A client can send a STOP_SENDING frame to tell the server it no longer wants data on a particular stream, and the server can send RESET_STREAM to abandon sending. These per-stream error signals are one of the advantages over TCP, where aborting one logical flow means tearing down the entire connection.
Stream Frames
Application bytes move in STREAM frames. Each frame carries:
- stream ID
- offset within that stream
- data bytes
- optional FIN flag
Because stream offsets are explicit, QUIC can place stream fragments into different packets without depending on packet ordering for stream reconstruction.
Example:
Packet 900: STREAM id=4 offset=0 len=1200
Packet 901: STREAM id=8 offset=0 len=900
Packet 902: STREAM id=4 offset=1200 len=600If packet 901 is lost, stream 4 still progresses. Only stream 8 waits for its missing bytes.
Stream Concurrency Limits
Endpoints negotiate how many streams the peer may open concurrently using the initial_max_streams_bidi and initial_max_streams_uni transport parameters during the handshake. After the handshake, the MAX_STREAMS frame can raise these limits dynamically.
This is an important flow control mechanism that TCP does not have. A misbehaving client that tries to open thousands of concurrent streams can be bounded by the server's policy. If a client exceeds the advertised stream limit, the server can close the connection with a STREAM_LIMIT_ERROR.
8. Flow Control in QUIC: Connection-Level and Stream-Level
QUIC implements flow control similarly in spirit to TCP, but with more granularity. TCP only protects the receiver with connection-level receive window semantics. QUIC has:
- per-stream flow control
- per-connection flow control
This matters because one badly behaved stream should not necessarily consume all buffering for the whole connection.
The receiver advertises limits through frames such as:
MAX_DATA, connection-wide byte limitMAX_STREAM_DATA, per-stream byte limitMAX_STREAMS, stream count limits
Suppose a browser opens a QUIC connection and the server starts streaming:
- HTML on stream 0
- CSS on stream 4
- 60 MB video chunk on stream 12
Without per-stream flow control, the video stream could monopolise buffer growth and create unnecessary pressure. With per-stream limits, the endpoint can bound how much data each stream is allowed to have outstanding.
How Credit-Based Flow Control Works
Flow control in QUIC is credit-based. The receiver tells the sender the maximum absolute byte offset it is willing to accept on a given stream (via MAX_STREAM_DATA) and across the entire connection (via MAX_DATA). The sender tracks how much it has sent and stops when it reaches the limit.
When the application consumes data from the receive buffer, the receiver can issue a new MAX_STREAM_DATA or MAX_DATA frame with a higher limit. If the sender runs out of credit, it can signal that with a DATA_BLOCKED or STREAM_DATA_BLOCKED frame. These blocked signals are not errors; they are informational. They tell the receiver "I have data to send but you have not given me permission yet," which can be useful for debugging and for prompting the receiver to issue new credits sooner.
A practical example: suppose the receiver initially advertises MAX_STREAM_DATA of 65536 bytes for stream 4. The sender transmits 65536 bytes and then stops. The receiver's application reads 32768 bytes from the buffer, freeing space. The receiver sends a new MAX_STREAM_DATA frame for stream 4 with a limit of 98304 (65536 + 32768), and the sender can resume.
This is one of those features application developers rarely think about directly, but they feel it when transport behaviour under load becomes more predictable.
9. Loss Detection and ACK Handling
QUIC loss recovery is defined separately from congestion control, though they interact closely. The design borrows heavily from modern TCP practice, especially RACK-style timing ideas, but QUIC uses its own acknowledgment format and packet-based logic.
ACK frames do not acknowledge bytes. They acknowledge packet number ranges.
An ACK frame might effectively mean:
- received packet 120
- received packets 122 through 130
- missing 121 so far
That is more expressive than basic cumulative TCP ACKs, and it does not require SACK as an optional extension because range-based acknowledgment is fundamental to QUIC.
ACK Frame Structure
The ACK frame itself has a specific wire format:
ACK Frame {
Largest Acknowledged (variable-length integer),
ACK Delay (variable-length integer),
ACK Range Count (variable-length integer),
First ACK Range (variable-length integer),
ACK Range (..) ...
}The Largest Acknowledged field gives the highest packet number received. The First ACK Range gives the count of contiguous packets before that one (also received). Each additional ACK Range encodes a gap (missing packets) followed by a run of received packets. This compact representation can describe complex patterns of received and missing packets efficiently.
For example, to encode "received 120, 122-130":
Largest Acknowledged: 130
ACK Delay: 500 (microseconds, scaled)
ACK Range Count: 1
First ACK Range: 8 (packets 122..130)
Gap: 0 (1 missing packet: 121)
ACK Range: 0 (packet 120)ACK Delay
Receivers can intentionally delay ACKs slightly and report the delay. This helps the sender distinguish network RTT from receiver-induced delay when computing RTT estimates.
QUIC implementations still have to be careful here. Over-delayed ACKs can slow loss detection and make bursty traffic look healthier than it is. Under-delayed ACKs waste bandwidth and CPU on pure acknowledgment traffic. This is one of those details that sounds minor in the specification and then turns into measurable behaviour at scale.
Loss Detection Criteria
QUIC usually declares loss based on:
- packet threshold, sufficiently newer packets acknowledged beyond a gap
- time threshold, packet outstanding longer than expected relative to RTT
In spirit:
If packet 200 is unacknowledged
and packets 201, 202, 203 are acknowledged,
packet 200 is probably lost.Or:
If packet 200 has been outstanding far longer than the smoothed RTT and reordering allowance,
packet 200 is probably lost.The default packet reordering threshold in QUIC is 3, meaning a packet is declared lost if three later packets have been acknowledged. The time-based threshold is typically 9/8 of the smoothed RTT (the "time threshold" or kTimeThreshold). These values can be tuned per implementation, and some stacks adjust them dynamically based on observed reordering patterns.
The sender then retransmits the relevant frames in a new packet.
Probe Timeout (PTO)
When no ACKs arrive at all, the sender cannot use gap-based or time-based loss detection because there are no acknowledgments to trigger them. For this case, QUIC uses the Probe Timeout (PTO). When the PTO fires, the sender transmits a probe packet containing ack-eliciting frames to force the peer to respond. This is conceptually similar to TCP's retransmission timeout (RTO), but QUIC's PTO is more aggressive about probing and less aggressive about declaring loss. The PTO does not automatically mark packets as lost; it just ensures the connection stays alive and gives loss detection another chance to work with fresh acknowledgment data.
Why This Feels Better Than TCP in Practice
Because QUIC has packet numbers, ACK ranges, and explicit encrypted transport metadata under its own control, implementations can evolve loss logic faster than kernel TCP stacks. That has been one of QUIC's quiet advantages. The protocol is modern, but so is its deployment model.
10. Congestion Control: QUIC Does Not Escape Physics
QUIC avoids some TCP design constraints, but it does not bypass network congestion. It still has to decide:
- how much data may be in flight
- when to grow the sending rate
- when to cut back after loss or ECN signals
Most QUIC stacks use congestion control algorithms conceptually similar to TCP's modern families:
- CUBIC
- BBR
- Reno variants, less common in production
The protocol does not mandate one universal algorithm. That is a feature. Different implementations can choose what works best for their environments.
Congestion Window Still Exists
The sender maintains a congestion window, usually called cwnd. Data in flight is bounded by the minimum of:
min(congestion_window, flow_control_limits)This is the same high-level rule as in TCP:
- receiver flow control can stop you
- network congestion control can stop you
- the smaller limit wins
CUBIC vs BBR in QUIC
CUBIC is the default congestion control for most QUIC implementations, inherited from its long dominance in Linux TCP. It uses a cubic function of time since the last congestion event to determine the window size, which gives it good performance on high-bandwidth, high-latency paths. CUBIC is loss-based: it reacts to packet loss by reducing the window.
BBR (Bottleneck Bandwidth and Round-trip propagation time) takes a different approach. Instead of reacting to loss, BBR actively probes for the available bandwidth and minimum RTT, then paces packets to match. BBR cycles through phases: probing bandwidth by briefly increasing the send rate, then draining any queue it created, then cruising at the estimated rate. Google's QUIC deployment has used BBR extensively, and BBRv2 addresses some of the fairness concerns raised about the original BBRv1.
The practical difference: on paths with shallow buffers (common on European mobile networks served by Deutsche Telekom, Vodafone, or Orange), BBR can achieve higher throughput because it does not need to fill the buffer until it overflows to discover the available rate. CUBIC, on the other hand, can be more conservative on these paths, growing its window slowly until it sees loss. On paths with deep buffers, BBR and CUBIC often converge to similar performance, though their queue occupancy patterns differ.
Because QUIC runs in user space, switching congestion control algorithms is a library update, not a kernel recompile. A CDN operator running edge nodes in Frankfurt and Amsterdam can deploy BBRv2 to their QUIC stack, measure the results for a week, and roll back if the numbers look wrong. Doing the same with kernel TCP requires OS-level changes and more careful rollout. This deployment flexibility is one of QUIC's most underappreciated advantages.
Initial Window and Pacing
Modern QUIC implementations typically combine congestion window management with pacing, which means they do not dump a burst of packets as fast as the CPU can queue them. They spread sends over time based on the estimated rate.
This matters on access links with shallow buffers, home routers, and mobile networks. Pacing often reduces microbursts and makes loss behaviour less ugly.
If a sender has permission to put 12 packets in flight, pacing does not mean "wait until only one packet remains and then dump 12 more." It means those packets are spread over the estimated send interval. That tends to reduce queue spikes on consumer equipment, which is one reason modern transport stacks care so much about pacing even when the congestion control algorithm itself has not changed.
ECN Support
QUIC supports Explicit Congestion Notification (ECN), which allows routers to signal congestion by marking IP packets rather than dropping them. When a QUIC receiver detects ECN-CE (Congestion Experienced) markings, it reports the count in its ACK frames using the ECN section. The sender can then react to congestion before loss occurs.
ECN support in QUIC is more robust than in TCP because QUIC validates the ECN path during connection establishment. If middleboxes strip or corrupt ECN bits (which happens on some networks), QUIC detects this and falls back to non-ECN behaviour. This validation step avoids the problem TCP sometimes faces where ECN negotiation succeeds but the actual markings are unreliable.
11. 0-RTT: Faster, but With Replay Risk
QUIC can support 0-RTT data when a client reconnects to a server it has talked to before and has cached session resumption state from TLS 1.3.
That means the client can send application data immediately with the first flight, without waiting a round trip for the server handshake to complete.
This is powerful for workloads like:
- repeat visits to the same site
- API calls from mobile apps
- CDN edge interactions
But it comes with a serious caveat: 0-RTT data can be replayed by an attacker who captures and re-injects it within the limits of the server's anti-replay policy.
How 0-RTT Works in Practice
During a previous connection, the server issues a NewSessionTicket message containing an opaque ticket encrypted with a key only the server knows. The client stores this ticket along with the transport parameters and resumption secret from that session.
On reconnection, the client includes the ticket in its ClientHello and derives 0-RTT keys from the resumption secret without waiting for the server's response. It can immediately send STREAM frames encrypted with these 0-RTT keys in the same flight as the ClientHello.
The server, upon receiving the ClientHello with a valid ticket, can derive the same 0-RTT keys and decrypt the early data. If the ticket is invalid, expired, or the server chooses not to accept 0-RTT, the early data is simply discarded and the handshake proceeds as a normal 1-RTT exchange.
Because of that, 0-RTT is only safe for operations that are replay-tolerant. Good candidates include:
- idempotent GET requests
- cacheable requests
- metadata fetches
Bad candidates include:
- money transfers
- state-changing POSTs
- one-time token consumption
Servers must implement anti-replay mechanisms. Common strategies include keeping a window of recently seen 0-RTT tickets (so duplicates are rejected) or limiting the time window during which 0-RTT tickets are accepted. Some implementations use a strike register: a data structure that records ticket usage and rejects any ticket seen before. The tradeoff is between the size of this state and the time window of vulnerability.
This is not a QUIC flaw. It is the consequence of allowing encrypted application data before full handshake confirmation.
12. Version Negotiation
QUIC includes a version negotiation mechanism to handle the case where a client and server support different protocol versions. When a server receives a packet with a version it does not support, it can respond with a Version Negotiation packet listing the versions it does support.
The Version Negotiation packet is unusual: it has no packet number, is not encrypted, and is not authenticated. This means an on-path attacker could forge one. To defend against this, QUIC v2 (RFC 9369) introduces compatible version negotiation, where the server advertises supported versions inside the encrypted handshake via a transport parameter. The client can verify that the version it ended up using is consistent with what the server actually supports, detecting any forged Version Negotiation packets retroactively.
QUIC version 1 (RFC 9000) uses version number 0x00000001. QUIC version 2 (RFC 9369) uses 0x6b3343cf. The two are semantically identical in terms of features, but v2 uses different salt values for Initial packet protection and different HKDF labels. This means implementations cannot accidentally cross-decrypt between versions, which was a deliberate design decision to enable protocol ossification testing.
The version negotiation mechanism also reserves a range of version numbers (0x?a?a?a?a, where ? is any hex digit) for "greasing." Implementations are encouraged to advertise these fake versions occasionally to ensure that middleboxes and peers do not become rigid about specific version values. This greasing pattern, borrowed from TLS, prevents the kind of ossification that has made TCP extension deployment so painful.
13. Header Protection and Why QUIC Looks Opaque on the Wire
TCP headers are mostly visible on the wire. QUIC encrypts much more aggressively.
QUIC protects:
- payload
- frame structure
- most header fields
It also applies header protection, which masks parts of the packet header using cryptographic material derived from the packet itself.
The header protection algorithm works by sampling a portion of the encrypted payload, feeding it into an encryption function (AES-ECB for AES-based cipher suites, or ChaCha20 for ChaCha-based suites), and using the output to mask the packet number length bits and the packet number itself. The result is that even the packet number, which sits in the unencrypted header area, is not readable without first decrypting a sample of the payload.
This creates a chicken-and-egg situation for anyone trying to parse QUIC passively: you need the packet number to decrypt the payload, but you need to decrypt a payload sample to unprotect the packet number. Without the keys, you cannot do either.
The result is that passive observers can see much less transport metadata than they could with TCP. That is good for privacy and protocol agility, but it makes old-style network debugging harder.
Operators cannot rely on casually reading transport flags in packet captures the way they used to with TCP. Instead they often need:
- endpoint logs
- qlog traces
- key logging for controlled decryption in debugging environments
- metrics emitted by the QUIC stack itself
This is one of QUIC's social costs. Better privacy and evolvability mean less middlebox visibility.
qlog: The Standard Debugging Format
Because packet captures are less useful for QUIC, the community developed qlog (draft-ietf-quic-qlog), a structured event logging format specifically designed for QUIC and HTTP/3. qlog records events like packet sent, packet received, frames parsed, congestion window updates, and stream state changes in a JSON-based schema that tools can visualise.
The companion tool qvis (https://qvis.quictools.info/) renders qlog files as interactive sequence diagrams, congestion graphs, and multiplexing timelines. For operators debugging QUIC performance issues on their edge servers in Stockholm or Milan, qlog and qvis have become the practical replacements for tcpdump and Wireshark's TCP stream analysis.
14. Connection Migration and NAT Rebinding
Connection migration is the headline feature. NAT rebinding is the everyday feature.
A device does not need to change networks dramatically to benefit from QUIC's connection model. Many consumer NATs and mobile carriers can remap source ports during the life of a flow. TCP treats that badly because the 4-tuple changed. QUIC can often survive it because the CID still points to the same connection.
What the Server Must Watch
When a packet arrives from a new address or port, the server should not instantly trust it. It needs to ask:
- is this a legitimate path change
- is this a spoofed packet
- should congestion state be reset or reduced for the new path
QUIC implementations often maintain per-path state for:
- RTT
- congestion control
- validation status
The new path should not necessarily inherit the old path's full sending aggressiveness. Moving from fibre in Berlin to a mobile network on a train outside Milan changes the path characteristics a lot.
Connection ID Rotation During Migration
When a client migrates to a new path, it should use a new Connection ID. If the client kept using the same CID from the old path, an observer who saw traffic on both paths could link them and track the client's movement across networks. QUIC addresses this by allowing each endpoint to issue multiple CIDs via NEW_CONNECTION_ID frames and retire old ones via RETIRE_CONNECTION_ID.
A typical migration flow looks like this:
- The server pre-supplies the client with several CIDs using NEW_CONNECTION_ID frames during normal operation.
- The client switches from Wi-Fi to mobile. It picks a fresh CID from its pool and sends its next packet from the new address using the new CID.
- The server receives the packet, looks up the connection by the new CID, and initiates path validation by sending a PATH_CHALLENGE.
- The client responds with PATH_RESPONSE on the new path, proving reachability.
- The server validates the path and begins sending on it. It resets the congestion window for the new path to the initial value, since the new path may have completely different characteristics.
- The client retires the old CID with RETIRE_CONNECTION_ID so it is not reused.
This CID rotation mechanism is critical for privacy. Without it, QUIC's migration feature would be a tracking vector.
15. HTTP/3 on Top of QUIC
HTTP/3 is not QUIC. HTTP/3 is HTTP semantics mapped onto QUIC transport.
The reason HTTP/3 matters here is that it finally lets HTTP multiplexing happen on a transport that understands independent streams.
What HTTP/3 Reuses
HTTP semantics remain familiar:
- methods
- status codes
- headers
- bodies
What Changes
Transport changes significantly:
- no TCP connection
- no TLS records over TCP
- request-response pairs on QUIC streams
- QPACK instead of HPACK for header compression
QPACK exists because HTTP/2's HPACK design assumed ordered delivery on one byte stream. QUIC streams are independent, so header compression had to be adapted to avoid reintroducing cross-stream blocking through compression dependencies.
That is an important pattern with QUIC: once the transport model changes, adjacent protocols often need redesign too.
QPACK Header Compression in Detail
HPACK, the header compression scheme used in HTTP/2, maintains a dynamic table that both encoder and decoder update in lockstep as headers are processed. Because HTTP/2 runs over TCP, which guarantees ordered delivery, the encoder and decoder always see the same sequence of updates. QPACK cannot rely on that guarantee because QUIC streams are delivered independently.
QPACK solves this with a three-stream architecture:
- Request streams carry the actual encoded headers for each HTTP request/response pair.
- Encoder stream (unidirectional, from encoder to decoder) carries dynamic table insert and duplicate instructions.
- Decoder stream (unidirectional, from decoder to encoder) carries acknowledgments that tell the encoder which dynamic table entries the decoder has processed.
When the encoder wants to reference a dynamic table entry on a request stream, it can either use an "unblocked" encoding that only references entries the decoder has already acknowledged, or it can use a "potentially blocking" encoding that references entries the decoder might not have received yet. In the second case, the decoder must buffer the header block until the referenced entry arrives on the encoder stream.
The tradeoff is between compression ratio and head-of-line blocking risk. An aggressive QPACK encoder achieves better compression by referencing entries the decoder might not have yet, but it reintroduces a form of cross-stream dependency. A conservative encoder avoids this by only referencing acknowledged entries, at the cost of slightly larger headers. Most production deployments use a moderate policy: they reference recent entries that are very likely to have arrived but do not wait for explicit acknowledgment of every single insert.
Why HTTP/3 Still Has Some Blocking
It is worth being precise here. QUIC removes transport-level head-of-line blocking across streams. It does not abolish every form of waiting inside HTTP.
An HTTP/3 request can still wait on:
- application prioritisation decisions
- server scheduling
- QPACK dynamic table dependencies if used badly
- backend latency behind the origin
What QUIC removes is the specific case where one lost transport packet freezes unrelated streams because they all share one TCP byte stream. That is a huge improvement, but not magic.
16. Security Considerations
QUIC was designed with security as a first-class concern, not bolted on afterward. But running a new transport over UDP introduces attack surfaces that TCP did not have in the same form.
Amplification Attacks
The most significant threat during the handshake is amplification. An attacker sends a small spoofed packet to a QUIC server, and the server responds with a much larger handshake flight directed at the spoofed source address. The 3x anti-amplification limit mitigates this, but servers with large certificate chains can still produce meaningful amplification within that budget.
The Retry mechanism provides a stronger defence. When a server is under load or suspects amplification, it can respond to the client's Initial packet with a Retry packet containing a token. The client must echo this token in a new Initial packet, proving it can receive packets at its claimed address. Only then does the server proceed with the full handshake. The cost is one additional round trip, which makes the handshake 2-RTT instead of 1-RTT.
Denial of Service
QUIC servers must be careful about state commitment. A flood of Initial packets, each requiring the server to derive Initial keys and process a ClientHello, can consume significant CPU. Unlike TCP SYN floods (which can be mitigated with SYN cookies that avoid server-side state), QUIC Initial processing is more expensive because it involves cryptographic operations.
Mitigations include:
- Using Retry tokens during high-load periods to validate client addresses before committing resources
- Rate-limiting Initial packet processing per source IP
- Offloading Initial packet validation to dedicated hardware or software layers
On-Path vs Off-Path Attackers
QUIC's encryption protects against off-path attackers who can inject but not observe traffic. They cannot forge valid packets because they do not have the encryption keys. However, on-path attackers who can both observe and inject traffic can still perform connection reset attacks by injecting a CONNECTION_CLOSE frame that passes decryption (though they would need to have the correct keys, which they do not for 1-RTT data). In practice, an on-path attacker who can drop or modify packets can disrupt any transport protocol, and QUIC does not claim to prevent that.
17. Real-World Deployment and Performance Data
QUIC is no longer experimental. Major European and global CDN operators have been running QUIC in production for years, and the performance data tells a consistent story.
Deployment Numbers
Cloudflare reported that as of late 2025, roughly 30% of HTTP requests to their network used HTTP/3 over QUIC. Akamai's European edge nodes in Frankfurt, London, and Amsterdam show similar adoption rates, with mobile clients using QUIC at higher rates than desktop clients because mobile browsers (particularly Chrome and Safari on iOS) prefer QUIC when available.
Fastly, whose QUIC implementation is based on the H2O library, has published data showing that QUIC reduces time-to-first-byte by 10-30% compared to HTTP/2 over TCP for clients on high-latency or lossy paths. The improvement is most pronounced for the initial connection (where the 1-RTT handshake saves a full round trip compared to TCP+TLS) and for repeat connections using 0-RTT.
Where QUIC Helps Most
The benefits are not uniform. QUIC's advantages are largest when:
- Latency is high: a user in Athens connecting to a CDN edge in Frankfurt saves one full RTT on every new connection. At 40ms RTT, that is 40ms. At 150ms (intercontinental), it is 150ms.
- Packet loss is moderate: at 1-2% loss, QUIC's stream independence avoids the cascading stalls that HTTP/2 over TCP suffers. Measurements from Vodafone's mobile network in Germany showed HTTP/3 page loads completing 15-25% faster than HTTP/2 under 1% random loss conditions.
- The client migrates: mobile users on trains (the Amsterdam-to-Berlin ICE route is a classic example) experience frequent path changes. QUIC connections survive these changes; TCP connections must re-establish.
Where QUIC Helps Less
On low-latency, low-loss wired connections (a developer on fibre in Copenhagen connecting to a server in the same city), the difference between QUIC and TCP+TLS 1.3 is often negligible after the first connection. The handshake savings of one RTT matter less when the RTT is 5ms. Stream independence matters less when loss is effectively zero.
QUIC can also underperform TCP on paths where UDP is throttled or deprioritised. Some enterprise firewalls and network equipment still treat UDP traffic as second-class, applying rate limits or even blocking it entirely. QUIC implementations handle this by racing QUIC and TCP connections in parallel (Happy Eyeballs style) and using whichever completes the handshake first.
18. Real-World Operational Tradeoffs
QUIC improves several things, but it also creates new operational issues.
Benefits
- faster connection establishment
- no transport-level head-of-line blocking across streams
- better mobility support
- faster protocol evolution
- integrated modern crypto by default
Costs
- more CPU work in user space
- UDP filtering or rate-limiting problems in some networks
- more opaque packet traces
- more implementation complexity in applications and libraries
- different DDoS and amplification considerations
Why CPU Matters
Kernel TCP is heavily optimised and benefits from decades of tuning, offload paths, and operating system integration. QUIC stacks do more in user space and encrypt more metadata. For large servers, CPU cost per connection can matter.
Benchmarks from QUIC implementations like quiche (Cloudflare), mvfst (Meta), and ngtcp2 show that QUIC typically uses 2-3x more CPU per byte transferred compared to kernel TCP with TLS 1.3. The gap is narrowing as implementations mature and as techniques like GSO (Generic Segmentation Offload) and io_uring are applied to QUIC. Some implementations now use kernel bypass (DPDK or XDP) for the UDP layer, which recovers much of the performance gap.
That said, operators often accept that trade because lower handshake latency, better multiplexing behaviour, and easier deployment of transport changes produce better end-user performance.
Another practical cost is offload maturity. TCP has decades of NIC and kernel optimisation behind it: segmentation offload, checksum offload, receive-side scaling, and lots of operational tooling. QUIC can benefit from some of the same hardware indirectly because it still rides inside UDP and IP, but the overall ecosystem is younger. That affects profiling, observability, and sometimes capacity planning.
19. What Developers and Operators Actually Notice
Developers usually do not notice QUIC packet number spaces or path challenge frames. They notice symptoms:
- mobile sessions survive network changes more often
- websites feel less fragile under moderate packet loss
- APIs reconnect faster
- protocol improvements ship inside libraries, browsers, and proxies
Operators notice different symptoms:
- UDP traffic share rises
- observability needs move toward endpoint logging
- load balancers need QUIC-aware routing strategies
- CID handling matters for connection steering
- anti-amplification and path validation become real operational concerns
A reverse proxy terminating QUIC at the edge in Amsterdam has to think about issues that did not exist in the same form for classic TLS over TCP:
- how to route packets by CID
- how to rotate server-issued connection IDs
- how much state to keep per path
- how to expose useful telemetry without full packet visibility
QUIC is better transport for modern internet applications, but it is not "same old transport, just faster."
20. A Minimal Mental Model of QUIC
If you want one compact mental model, use this:
QUIC is a secure, user-space transport that uses UDP only to get packets across the network. It numbers packets, retransmits frames rather than packets, multiplexes independent streams, integrates TLS 1.3 into the transport handshake, uses connection IDs so sessions can survive path changes, and gives applications a transport stack that can evolve much faster than TCP.
That model explains most of the protocol's design decisions:
- QUIC uses UDP because TCP is too ossified
- QUIC integrates TLS because encrypted transport is the normal case
- QUIC uses streams because one global ordered byte stream caused blocking
- QUIC uses CIDs because modern clients move between paths
- QUIC lives in user space because transport iteration speed matters
If you approach it as "encrypted multiplexed transport with modern deployment assumptions," the pieces fit together.
21. Why QUIC Will Keep Spreading
The strongest argument for QUIC is not that it beats TCP in every benchmark. It is that it aligns transport design with how internet applications are actually shipped in 2026.
Browsers update constantly. CDNs update constantly. Reverse proxies update constantly. Mobile apps update constantly. Kernel transport evolution is too slow and too constrained for that environment. QUIC lets transport behaviour evolve with the application stack.
That does not mean TCP disappears. Databases, internal services, SSH, email, and enormous amounts of infrastructure will keep relying on TCP for a long time. But for latency-sensitive, encrypted, internet-facing application traffic, QUIC fits the problem unusually well.
HTTP/3 needed a transport that understood streams, encryption, and mobility as first-class concepts. TCP could not become that transport without dragging decades of wire compatibility behind it. QUIC could.