QUIC: the transport hiding inside UDP
Why a new transport was needed, how QUIC folds the TCP and TLS handshakes into one, streams that do not block each other, packet numbers, connection migration, HTTP/3, and what it costs.
On this page
QUIC is TCP and TLS redesigned together, with thirty years of lessons applied, and then smuggled across the internet inside UDP packets so that nothing in between has to change. It is what HTTP/3 runs on, which means it already carries a large share of web traffic. This page is about what it fixes, how the pieces fit, and what you give up.
The map
Read this first when short on time. Every branch is a section below.
Why anyone would build a new transport
TCP works. It has worked for forty years. But three of its problems could not be fixed in place, and by 2012 they were costing real time on every page load.
One lost packet stalls everything. TCP is a single ordered stream. HTTP/2 puts many requests on one connection, so when one packet is lost, every request on that connection waits for the retransmit, even the ones whose data has already arrived. The TCP article calls this head-of-line blocking. On a lossy mobile link it makes HTTP/2 slower than the HTTP/1.1 it replaced.
Two handshakes before a byte. TCP needs a round trip to connect, and then TLS needs another to agree on keys. On a 200 ms path that is 400 ms of waiting before the request is even sent, as the packet journey adds up.
A connection is tied to four numbers. Source address, source port, destination address, destination port. When your phone walks out of Wi-Fi range and onto cellular, its address changes, every TCP connection dies, and every app reconnects.
None of these can be patched into TCP. The header is visible to every router and firewall on the path, and many of them inspect it and drop anything unfamiliar; the protocol is frozen by the middleboxes that watch it. This is called ossification. And TCP lives in the operating system kernel, so a change ships when every OS updates, which is years.
So Google built a transport that avoids both traps. It runs in user space, inside the browser or the server program, so it ships with the app. It travels in UDP datagrams, which every middlebox already passes. And it encrypts almost the entire header, so no middlebox can inspect it, depend on it, or freeze it. The UDP article explains why UDP was the only usable base: it does nothing, which is exactly what a new transport needs underneath.
- Google shipped the first QUIC in Chrome in 2013 for its own sites, then handed it to the IETF, which published the standard version in 2021.
- HTTP/3 is HTTP over QUIC, and it is served by default by Cloudflare, Google, Meta and most large CDNs, so a good share of web traffic is already QUIC.
- Middleboxes are why the QUIC header is encrypted even where it did not need to be: the designers had watched TCP options die at the hands of firewalls that dropped anything new.
- Head-of-line blocking
- One lost packet holding back data behind it that has already arrived.
- Ossification
- A protocol becoming impossible to change because devices in the middle depend on its current shape.
- User space transport
- A transport protocol implemented in the application or a library instead of the kernel.
One handshake for transport and encryption
QUIC does not have a TLS handshake after a transport handshake. It has one handshake that does both. The TLS 1.3 messages travel inside the very first QUIC packets, so the keys are agreed in the same round trip that establishes the connection.
sequenceDiagram autonumber participant C as Client participant S as Server Note over C,S: TCP plus TLS 1.3: two round trips C->>S: SYN S-->>C: SYN-ACK C->>S: ACK, ClientHello S-->>C: ServerHello, certificate, Finished C->>S: Finished, GET / Note over C,S: QUIC: one round trip C->>S: Initial: ClientHello S-->>C: Initial and Handshake: ServerHello, certificate, Finished C->>S: Finished, GET /
- The client sends an Initial packet with the TLS ClientHello inside: supported ciphers, its key share, the server name. The packet is padded to at least 1,200 bytes, which proves the path can carry that size and stops a tiny packet from triggering a large reply.
- The server replies with the ServerHello, its certificate and its own key share, and its own transport parameters. Both sides now derive the keys. The server may already send application data encrypted with them.
- The client confirms with a Finished message, and its first request goes in the same packet. One round trip, and the request is on the wire.
On a repeat visit it is faster still. The client kept a ticket from the last session and can encrypt data with keys derived from it before hearing from the server at all. This is 0-RTT: the first packet carries the request. The catch is that an attacker who captured that packet can replay it, and the server cannot tell, so 0-RTT data must be safe to process twice. A GET is fine. A payment is not. Browsers send only idempotent requests this way, and servers may reject 0-RTT for anything else.
One more rule protects against amplification, the attack the UDP article describes. Until the client has proved it really is at the address it claims, by echoing something the server sent, the server may send at most three times the bytes it received. That is why the first client packet is padded to 1,200 bytes: it buys the server room to reply with its certificate.
- Chrome connecting to Google and YouTube uses 0-RTT on nearly every visit; the search query is in the first packet.
- Cloudflare measured the handshake saving directly: pages over HTTP/3 start rendering about one round trip earlier than over HTTP/2 on the same path.
- Retry packets let a server under attack force a round trip before doing any expensive work, by handing the client a token to come back with.
0-RTT data is replayable by design. Any server accepting it must treat those requests as possibly duplicated: safe for reads, dangerous for anything that changes state. Most servers accept 0-RTT only for GET and HEAD.
- Initial packet
- The first QUIC packet, carrying the TLS ClientHello. Encrypted with keys anyone can derive, which stops casual inspection but not a determined observer.
- 0-RTT
- Sending application data in the very first packet of a resumed connection, using keys from the previous session.
- Address validation
- Proving a client can receive at the address it sends from, before the server sends more than three times what it got.
Streams: many independent conversations in one connection
Inside one QUIC connection there are many streams, and each one is its own ordered, reliable byte stream. A lost packet delays only the streams whose data was in it. Every other stream keeps flowing. This is the fix for head-of-line blocking, and it is the single biggest difference from TCP.
flowchart LR P1["packet 7<br/>stream 0: 0 to 800<br/>stream 4: 0 to 400"] --> R P2["packet 8<br/>stream 4: 400 to 1200"] -. lost .-> R P3["packet 9<br/>stream 8: 0 to 1000"] --> R P4["packet 10<br/>stream 0: 800 to 1600"] --> R R["Receiver"] --> S0["stream 0<br/>1600 bytes delivered"] R --> S4["stream 4<br/>400 delivered, waiting"] R --> S8["stream 8<br/>1000 bytes delivered"]
Streams are cheap to create: sending data on a new stream ID creates it, with no handshake. They are numbered so the two sides never collide: the low two bits of the ID say who opened it and whether it is one-way or two-way, so client-opened bidirectional streams are 0, 4, 8 and server-opened ones are 1, 5, 9. A stream ends with a FIN bit on its last frame, or is cut off with a RESET frame that says "stop, forget the rest". Resetting one stream costs nothing to the others, which is how a browser abandons one image download without touching the rest of the page.
Flow control works at two levels. Each stream has its own window, so one slow consumer cannot eat the whole connection's buffer, and the connection has a total window on top. Both are advertised with MAX_STREAM_DATA and MAX_DATA frames and grow as the receiver reads, like TCP's receive window in the TCP article but multiplied.
- HTTP/3 puts each request and response on its own bidirectional stream. A stalled image never delays the HTML.
- WebTransport exposes QUIC streams and datagrams directly to browser JavaScript, a lower-level alternative to WebSockets for games and live data.
- gRPC and other RPC systems have been experimenting with QUIC for the same reason: one slow call no longer holds back the others sharing the connection.
- Stream
- One ordered, reliable byte stream inside a QUIC connection. Independent of every other stream for loss and flow control.
- RESET_STREAM
- Abandon one stream immediately without affecting the connection.
Packets and frames: how loss is detected
A QUIC packet is an encrypted envelope with a number. Inside it are frames: pieces of stream data, acknowledgements, control messages. The separation of the two is what makes QUIC's loss recovery cleaner than TCP's.
| Frame | Carries |
|---|---|
STREAM | Bytes for one stream: stream ID, offset, length, data, optional FIN |
ACK | Ranges of packet numbers received, and how long the receiver held the largest one before acknowledging |
CRYPTO | TLS handshake messages |
MAX_DATA, MAX_STREAM_DATA | Flow control windows |
NEW_CONNECTION_ID | Extra identifiers for migration |
PING, PADDING | Keep the path alive, fill a packet |
CONNECTION_CLOSE | Shut down with an error code |
The rule that matters: a packet number is never reused. TCP retransmits a lost segment with the same sequence number, so when an ACK arrives the sender cannot tell whether it is for the first copy or the retransmit, and its round trip estimate is wrong. QUIC puts the retransmitted stream data into a fresh packet with a fresh number. Every ACK unambiguously names one transmission, so round trip times are exact, and loss is detected by gaps in the numbers rather than by guessing.
ACK frames list ranges, so one frame can say "I have 1 to 40, 42 to 57, and 60" and the sender knows exactly what to resend. It resends the frames that were in the lost packets, not the packets themselves, so an ACK frame that was lost is simply replaced by a newer one and stream data is repackaged however fits.
Congestion control is the same idea as TCP's: start small, grow while acknowledgements come back, cut back on loss. The standard describes a Reno-like algorithm and implementations use Cubic or BBR, the same names as in the Linux kernel. The difference is where it runs: in the library, in the application, tunable per deployment, upgradeable with a software release.
- qlog is the standard trace format; QUIC implementations write one and tools like qvis draw the packets, ACKs and congestion window as a chart, which is how you debug what Wireshark cannot decrypt.
- quiche (Cloudflare), msquic (Microsoft), ngtcp2, quic-go and Google's own are the main implementations; nginx and curl use one of them, and there is no QUIC in the Linux kernel.
- BBR in QUIC is how YouTube keeps a video flowing on a lossy link: it models bandwidth and delay instead of treating every loss as congestion.
- Frame
- One unit of content inside a packet: stream bytes, an ACK, a control message.
- Packet number
- A counter that never repeats within a connection, so every ACK is unambiguous.
- ACK range
- A run of packet numbers received, so gaps are described exactly.
Connection IDs and migration
A TCP connection is identified by its two addresses and two ports, so it cannot survive any of them changing. A QUIC connection is identified by a connection ID, a random token carried in every packet. The addresses are just where the packets happen to come from right now.
sequenceDiagram autonumber participant P as Phone participant S as Server P->>S: packets from Wi-Fi 192.0.2.5, connection ID ab12 Note over P: leaves the house, switches to cellular P->>S: packet from 198.51.100.9, same connection ID ab12 S->>P: PATH_CHALLENGE to the new address P-->>S: PATH_RESPONSE Note over S: new path confirmed, keep going S->>P: stream data continues, no reconnect
When the server sees a known connection ID arrive from a new address, it does not just trust it, because that would let an attacker redirect someone's download to a victim. It sends a PATH_CHALLENGE to the new address and waits for the echo. Once confirmed, the connection continues on the new path, with the congestion window reset because the new path may be very different.
The same mechanism handles the mundane case of a NAT quietly changing your port after a period of idleness, which on TCP would also kill the connection. And because each side hands the other several connection IDs in advance, a migrating client can switch to a fresh one, so an observer cannot link the Wi-Fi traffic to the cellular traffic by the ID.
- A phone leaving home mid-download over HTTP/3 keeps downloading; over HTTP/2 the app shows a spinner and retries.
- Load balancers that spread QUIC across servers encode the server's identity inside the connection ID, so a migrated packet still reaches the right machine; Facebook and Cloudflare both published how.
- Connection ID
- A random identifier in every packet that names the connection independently of addresses.
- Path validation
- A challenge and echo that prove a new address belongs to the peer before traffic moves to it.
HTTP/3: HTTP on top of it
HTTP/3 is the same HTTP the HTTP article describes, the same methods, headers and status codes, carried over QUIC instead of TCP. The differences are all in the plumbing.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Requests per connection | One at a time | Many, multiplexed | Many, one per QUIC stream |
| One lost packet stalls | That request | Every request on the connection | One stream |
| Handshake before first request | 2 round trips (TCP, TLS) | 2 round trips | 1, or 0 on a repeat visit |
| Header compression | None | HPACK | QPACK |
| Survives an IP change | No | No | Yes |
| Encryption | Optional | Required in practice | Built in |
Each request goes on its own bidirectional stream, so responses arrive in whatever order they are ready. Header compression had to change: HTTP/2's HPACK assumes headers arrive in order, which QUIC streams do not guarantee, so QPACK sends the shared dictionary updates on a separate stream and lets headers refer to entries only once they are known to have arrived.
A browser does not know a site speaks HTTP/3 until it is told. It connects over TCP first, and the server's response includes an Alt-Svc: h3=":443" header, after which the browser tries QUIC on the next connection and remembers the result. A newer way is an HTTPS record in DNS that announces it before any connection. If the QUIC attempt gets no reply, because a firewall drops UDP, the browser goes back to HTTP/2 without the user noticing.
- Every major browser speaks HTTP/3, and
curl --http3does too when built with a QUIC library. - nginx serves it with
listen 443 quic;next to the usual TCP listener; the two share the certificate and the configuration. - Devtools show the protocol per request in the network tab;
h3means QUIC carried it.
- QPACK
- HTTP/3's header compression, designed to work when streams arrive out of order.
- Alt-Svc
- A response header telling the client that the same service is available on another protocol, used to advertise HTTP/3.
What QUIC costs
Nothing in networking is free, and QUIC pays in three places.
CPU. TCP has forty years of hardware help: network cards segment and checksum for it, and the kernel path is tight. QUIC encrypts every packet individually, runs in user space with a system call per datagram or batch, and has fewer offloads to lean on. Servers report QUIC using two or three times the CPU of TCP for the same bytes, and the gap is closing as UDP offloads and kernel batching improve, but it has not closed.
Blocked UDP. A few percent of networks drop or throttle UDP, particularly on port 443. Every QUIC client carries a fallback to TCP and pays a wasted attempt when it is needed. Some enterprise networks block QUIC deliberately so their inspection proxies can keep seeing HTTP/2.
Visibility. The encrypted header that protects QUIC from ossification also hides it from operators. Wireshark shows a UDP flow; nothing in the middle can measure loss, retransmits or round trip times the way it can for TCP. The designers left one bit, the spin bit, that flips once per round trip so a passive observer can estimate latency, and many implementations do not set it.
And QUIC is not for everything. Inside a data centre, where loss is rare and the network is trusted, TCP with hardware offload is faster and cheaper. QUIC earns its cost on the paths it was designed for: lossy, long, mobile, and shared with middleboxes.
- Meta published that moving its mobile apps to QUIC cut request errors and tail latency noticeably, and that the CPU cost on servers was the main engineering effort.
- Linux added generic UDP segmentation offload partly for QUIC, so a server can hand the kernel one large buffer and have it cut into datagrams.
- Corporate networks that block UDP 443 see every browser silently use HTTP/2 instead, which is why a QUIC rollout can look like it did nothing.
Measuring a QUIC deployment by "average page load time" hides the win. QUIC helps most on the worst connections, so the improvement shows at the 95th and 99th percentiles, and not at all on a fast office link.
- Offload
- Work a network card does instead of the CPU: checksums, segmentation, sometimes encryption.
- Spin bit
- The one unencrypted QUIC header bit that lets an observer estimate round trip time.
Recap
- QUIC exists because TCP could not be fixed: head-of-line blocking across streams, two handshakes, and connections that die on an address change, all frozen by middleboxes and slow kernel updates.
- It runs in user space, inside UDP datagrams, with nearly the whole header encrypted so nothing in the middle can inspect or freeze it.
- The handshake carries TLS 1.3 inside the first packets: one round trip to send a request, zero on a repeat visit with replayable 0-RTT data.
- The first client packet is padded to 1,200 bytes and the server may send at most three times what it received until the address is validated.
- A connection holds many streams. A lost packet stalls only the streams it carried. Each request in HTTP/3 is one stream.
- Packet numbers are never reused, so every ACK is unambiguous and round trip times are exact. Lost frames are repackaged, not retransmitted as-is.
- Congestion control is TCP's ideas (Cubic, BBR) in the library.
- Connection IDs name the connection instead of addresses, so it survives NAT rebinding and moving from Wi-Fi to cellular, after a path check.
- HTTP/3 is HTTP over QUIC with QPACK headers, discovered via Alt-Svc or DNS, falling back to HTTP/2 over TCP when UDP is blocked.
- The costs are CPU, blocked UDP on some networks, and no visibility for operators. The win shows at the tail, on bad connections.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then a couple of points, then one extra detail.
What problems with TCP does QUIC solve?
Head-of-line blocking across multiplexed requests, the extra round trip of a separate TLS handshake, and connections that die when the client's address changes; and it does so in user space over UDP because TCP itself cannot be changed without middleboxes and kernels cooperating.
- Independent streams mean a loss stalls only its own stream.
- The TLS handshake rides inside the first packets, so the request goes out after one round trip, or zero on resumption.
The encrypted header is deliberate: it stops the next generation of middleboxes from freezing QUIC the way they froze TCP.
Why is QUIC built on UDP rather than as a new IP protocol?
Because firewalls and NATs pass only TCP and UDP, so a new protocol number would be dropped almost everywhere; UDP provides just ports and nothing else, which is exactly the blank base a user space transport needs.
- Running in user space lets it ship with the browser instead of waiting for operating systems.
- QUIC adds its own reliability, ordering and congestion control on top.
The cost is fewer hardware offloads and a fallback to TCP where UDP is blocked.
How does QUIC get from zero to a sent request in one round trip?
The first packet carries the TLS 1.3 ClientHello, the server's reply carries the ServerHello, certificate and keys, and the client's next packet carries its Finished message together with the application request, so the transport and encryption handshakes overlap completely.
- TCP plus TLS needs the TCP handshake to finish before the ClientHello can be sent, costing an extra round trip.
- A returning client with a session ticket sends the request in its very first packet: 0-RTT.
The first client packet is padded to 1,200 bytes so the server has bandwidth budget to reply with its certificate.
What is the danger of 0-RTT, and how do servers handle it?
0-RTT data can be captured and replayed by an attacker and the server cannot distinguish the replay, so it must only be used for requests that are safe to process twice; servers accept it for idempotent methods like GET and reject or delay it for anything that changes state.
- Browsers send only safe requests in 0-RTT.
- The rest of the connection, after the handshake completes, is not replayable.
Anti-replay windows on the server reduce but do not eliminate the risk, which is why the rule is about request semantics.
How do QUIC streams avoid head-of-line blocking?
Each stream is an independent ordered byte stream with its own offsets, so when a packet is lost only the streams that had data in it wait for the retransmit, and data for other streams in later packets is delivered immediately.
- HTTP/3 maps one request to one stream, so a stalled image never blocks the HTML.
- Each stream has its own flow control window under a connection-wide one.
A single stream still has in-order delivery; the guarantee is per stream, not across them.
Why does QUIC never reuse a packet number?
So every acknowledgement names exactly one transmission: TCP retransmits with the same sequence number and cannot tell which copy an ACK refers to, which corrupts its round trip estimate, whereas QUIC repackages lost frames into a new packet with a new number.
- Loss is detected from gaps in packet numbers and ACK ranges.
- Round trip measurements are exact, so timers and congestion control are more accurate.
Retransmission is of frames, not packets, so a lost ACK frame is just replaced by a newer one.
What is a connection ID and what does it enable?
A random identifier in every packet that names the connection independently of IP addresses and ports, so the connection survives a NAT changing the client's port or the client moving from Wi-Fi to cellular.
- The server validates a new path with a challenge and echo before trusting it, to prevent redirect attacks.
- Clients switch to a fresh connection ID when migrating so an observer cannot link the two paths.
Load balancers embed a server identifier in the ID so migrated packets reach the right backend.
How does a browser discover that a site supports HTTP/3?
The first response over TCP carries an Alt-Svc: h3 header, after which the browser tries QUIC on the next connection and remembers the result; newer clients can also learn it from an HTTPS record in DNS before connecting.
- If the QUIC attempt gets no reply, the browser falls back to HTTP/2 over TCP silently.
- Devtools show
h3as the protocol for requests that used it.
Blocked UDP on port 443 makes a QUIC deployment look like it did nothing on that network.
Why did HTTP/3 need a new header compression scheme?
HTTP/2's HPACK assumes headers arrive in order so both sides update a shared dictionary in lockstep, but QUIC streams can arrive out of order; QPACK moves dictionary updates to a separate stream and lets headers reference entries only after they are acknowledged.
- Static table entries for common headers are still used without any dependency.
- The trade-off is a little less compression in exchange for no blocking.
Everything else in HTTP/3, methods, status codes and semantics, is unchanged from HTTP/2.
How does QUIC avoid being used for amplification attacks?
Until the client proves it can receive at its claimed address, the server sends at most three times the bytes it received, and the client's first packet must be padded to 1,200 bytes; a server under load can also send a Retry packet with a token, forcing a round trip before any work.
- This closes the spoofed-source hole that plain UDP services have.
- Address validation is completed by the client echoing server-chosen data.
The padding also confirms the path can carry 1,200-byte datagrams, QUIC's minimum.
When would you not use QUIC?
Inside a data centre or on any trusted, low-loss, high-bandwidth path, where TCP with hardware offload uses far less CPU and QUIC's advantages (loss isolation, migration, one-trip handshake) barely matter.
- QUIC costs two to three times the CPU of TCP per byte on current hardware.
- Operators lose the ability to observe loss and latency in the middle.
Its wins appear on lossy, long, mobile paths and show up at the tail latencies, not the average.