Why does QUIC exist when TCP already works?
TCP works fine — until you're on a flaky phone connection, juggling a dozen multiplexed streams, and one lost packet stalls all of them. QUIC is the protocol designed around that specific frustration.
On this page
The picture version
Six pictures for a reader who has never thought about what a connection is. The prose below fills in the seams the pictures skip.
1 · The problem
A TCP connection is its four addresses. Change one and it’s dead.
2 · The second pressure
One lost packet, and everything unrelated waits behind it.
3 · Why you can’t just fix TCP
The network in the middle has opinions, and they are thirty years old.
4 · Encrypt it, or it ossifies too
If middleboxes can’t read the fields, they can’t grow opinions about them.
5 · The two payoffs
Independent streams, and a name the network can’t take away.
6 · Keep this card
The whole thing on one index card.
Why it exists
Picture this: you’re on a video call walking out of your house. Your phone hops from home Wi-Fi to mobile data. With most older internet protocols the call would drop right there — your phone’s “address” just changed, and the connection is defined by that address. With newer ones, the call can carry on across the switch instead of being torn down. That’s QUIC at work. Same goes for the moment a single packet is lost on flaky hotel Wi-Fi: with TCP, every other transfer sharing that connection waits for the missing byte. QUIC was designed so only the transfer that actually lost something has to wait.
For decades, the answer to “how do two computers reliably exchange a stream of bytes over the internet?” has been TCP. It works. It’s everywhere. So why did Google build an experimental replacement (gQUIC), and why did that work end up as a standardized protocol at the IETF?
Because TCP was designed in a world that no longer exists. Three things changed underneath it, and the friction kept growing:
- Connections move now. Your phone hops between Wi-Fi and cellular mid-page-load.
A TCP connection is keyed by
(source IP, source port, dest IP, dest port)— change any of those four and the connection is, by definition, dead. - Pages need many things at once. A modern page pulls dozens of resources in parallel. HTTP/2 tried to fix this by multiplexing many streams over a single TCP connection. But TCP is a single ordered byte stream — if one packet is lost, every stream above it has to wait. This is head-of-line blocking, and multiplexing over one TCP connection exposes it more sharply on lossy links than opening several connections did.
- Encryption is the default. TLS used to be optional on the web. Now browsers treat plain HTTP as an error state, and anything handling real data is expected to be encrypted. But TLS sits on top of TCP, so connecting takes a TCP handshake (one round trip) plus a TLS handshake (one or two more). On a satellite link or congested mobile, that’s painful before any actual data moves.
You could fix all of this inside TCP, in theory. In practice you can’t, because TCP lives in the kernel and middleboxes — routers, NATs, “smart” firewalls — have spent thirty years hard-coding assumptions about what TCP packets look like. Try to add a new TCP option and a chunk of the internet’s middle silently drops your packets. This is protocol ossification: the protocol can’t change because the network refuses to let it.
QUIC’s move is to side-step that entirely. Build a new transport on top of UDP, encrypt everything (including the parts middleboxes try to inspect), and put the protocol in userspace so applications can ship updates without waiting for the kernel.
Why it matters now
HTTP/3 is HTTP carried over QUIC, and the major browsers support it and negotiate it by default when the server offers it — which the large CDNs do. So if your site sits behind one of them, some fraction of your traffic is already QUIC whether or not anyone chose that; if it doesn’t, you’re still entirely on TCP.
For engineers, the practical consequences:
- Faster connection setup. QUIC bundles the transport handshake and the cryptographic handshake into one. First-time connections take roughly one round trip; resumed connections can take zero round trips for the first request bytes (so-called 0-RTT).
- Per-stream delivery. A loss on one stream doesn’t hold up the delivery of bytes on the others (it does still slow the whole connection down — congestion control is shared). HTTP/3 finally gets the multiplexing HTTP/2 promised.
- Connection migration. A QUIC connection has its own ID, decoupled from IP and port. Moving to a new network can keep the connection alive rather than forcing a new one — subject to path validation and to both endpoints supporting it, so it’s a capability, not a guarantee.
- Operationally different. QUIC traffic is opaque to most middleboxes,
which means a lot of “we look at TCP windows” tooling no longer works.
Debugging shifts toward endpoint logs and
qlogtraces.
The short answer
QUIC = UDP + TLS 1.3 + per-stream reliability + connection IDs
Picture to keep: instead of one conveyor belt where a jammed box stops everything behind it, a bundle of independent belts sharing one motor — and the whole bundle is tagged with a ticket number, so it can be picked up and plugged into a different power socket without stopping. Note the shared motor: that’s where the analogy is exactly right and where people expect too much. A jam on one belt no longer blocks the others, but everything still slows down when the motor throttles.
QUIC is a reliable, multiplexed, encrypted transport built on UDP instead of TCP. It folds the crypto handshake into the transport handshake, gives each logical stream its own loss recovery so a dropped packet doesn’t stall its neighbors, and identifies connections by an ID rather than the source/destination 4-tuple — so the connection survives when the network path underneath changes.
How it works
Take the walk out of your front door as the case to fix, and let each attempt fail into the next one.
Naive fix: patch TCP. Add a TCP option for “this connection moved,” add one for per-stream framing. This is the obvious answer and it’s the one that doesn’t work: TCP lives in the kernel, so shipping a change means shipping an OS to billions of devices, and even then middleboxes drop or mangle options they don’t recognize. You’d wait years to find out whether the fix survived the path.
Fix: stop asking permission — build on UDP. QUIC packets ride inside UDP datagrams. UDP gives you almost nothing — just “deliver this to that IP/port, maybe” — and that’s exactly the appeal. There’s so little structure for middleboxes to ossify against. The protocol logic that TCP does in the kernel (sequence numbers, acknowledgments, congestion control, retransmission) all moves into a QUIC library in userspace.
But moving to UDP re-opens the door to the same ossification. Ship a readable new transport and middleboxes will start parsing its fields within a few years, and then it’s frozen too — plus the connection is now completely unencrypted, since UDP brings nothing. Fix: make encryption part of the transport rather than a layer above it. In TCP+TLS the transport runs first, then TLS negotiates on top. In QUIC, almost every byte of every packet is encrypted or authenticated from the start, including fields a middlebox would historically read. The handshake reuses TLS 1.3’s key schedule but folds it into the transport’s own packets. The result is one unified handshake instead of two. “Everything” is a slight exaggeration: a small outer header stays visible so the packet can be routed and the connection identified, and the first Initial packets are protected with keys derived from public values, so an on-path observer can parse more of them than of later traffic. The point is that the fields middleboxes historically built policy on are no longer available to them.
This is also a deliberate anti-ossification move. If middleboxes can’t read the fields, they can’t grow assumptions about them, and the protocol stays free to evolve.
But a single reliable stream would just recreate head-of-line blocking. If QUIC delivered one ordered byte stream like TCP, a lost packet on the video call’s control channel would still stall every other transfer sharing the connection — you’d have rebuilt the HTTP/2 problem one layer down. Fix: make streams first-class and independent. A QUIC connection carries many streams, and each stream numbers its own bytes with its own offsets, so the receiver knows which stream a gap belongs to. Lose a packet carrying stream 7’s bytes and only stream 7 has a gap to wait on; streams 1–6 and 8–N keep handing bytes up to the application. (Loss detection and congestion control still operate on the connection as a whole — the independence is in delivery order, not in bandwidth.) This is the head-of-line blocking fix, and it’s the part HTTP/2 fundamentally couldn’t do, because the TCP underneath only knew about one stream.
And none of that helps when you step onto the street. Wi-Fi to LTE
changes your IP, and a connection identified by its address endpoints is
dead by definition — the call drops mid-sentence. Fix: name the connection
something the network can’t take away. Instead of (srcIP, srcPort, dstIP, dstPort), a QUIC
connection is identified by an opaque connection ID chosen during the
handshake. If your IP changes — Wi-Fi to LTE, NAT rebinding — the connection
ID is still valid, and once the endpoint has validated the new path (“is this
really you, and does traffic actually flow here?”) the connection can
continue rather than being rebuilt. Whether that happens in practice depends
on both ends implementing it; TCP can’t do it at all without protocol
surgery.
Show the seams
A few things the marketing version glosses over:
- QUIC isn’t free. Doing transport in userspace and encrypting every packet costs CPU. Early deployments saw meaningfully higher per-byte CPU cost than TCP+TLS, and a lot of subsequent work (kernel offloads, GSO/GRO for UDP, hardware crypto) has been about closing that gap. Published numbers vary so much with kernel version, NIC and library that no single figure represents the state of it — but “free upgrade” it isn’t.
- 0-RTT is a security tradeoff. Data sent in the zero-round-trip first flight can be replayed by an attacker who captured the packets, so it’s only safe for idempotent operations. This is a real footgun if an application doesn’t think about it.
- Some networks block or rate-limit UDP. Corporate networks especially. Browsers handle this by falling back to TCP when QUIC doesn’t get through (the details of how they decide differ by browser), but if you’re building a non-browser client you have to plan for it yourself.
- The “0 round trips on resumption” claim has caveats. It assumes a recent prior connection and acceptable security properties for the data. Cold connections still pay one round trip.
- “Google invented it” is shorthand. Google shipped the original gQUIC inside Chrome and their servers, and that work fed into the IETF standardization process that produced the version specified in RFC 9000. The deployed-on-the-internet QUIC today is the IETF version, not the original gQUIC, and they’re not wire-compatible.
You started with QUIC = UDP + TLS 1.3 + per-stream reliability + connection IDs. What did the walk out the front door add? — + UDP was chosen for what it doesn't have. Every headline feature follows from putting the transport
somewhere the kernel and the middleboxes can’t freeze it: that’s why the
crypto could be folded in, why streams could be split, why an ID could
replace the 4-tuple — and why QUIC pays in CPU for things TCP gets from
silicon and the kernel.
Check yourself
Before you go — a colleague moves an internal service from HTTP/1.1 to HTTP/3 and sees no improvement at all. The service runs inside one datacenter over a 0.2 ms link with essentially no packet loss. Why is that the expected result?
Answer
Almost everything QUIC buys you is priced in round trips and loss. Head-of-line blocking only hurts when packets are actually lost; the handshake savings only matter when an RTT is expensive; connection migration only matters when the client’s address changes. On a lossless sub-millisecond link none of those apply, and you’re left paying QUIC’s extra per-packet CPU for benefits the environment can’t deliver. QUIC is a fix for bad paths, not a universally faster transport.
And one more — if per-stream loss recovery removes head-of-line blocking, why can a lost QUIC packet still delay a stream that had nothing in it?
Answer
Streams are independent for delivery, not for capacity. All streams share one connection-level congestion controller, so a loss makes the whole connection slow down — every stream’s bytes now leave more slowly, including streams that lost nothing. What QUIC removes is the ordering dependency (you no longer wait for someone else’s missing byte), not the shared bottleneck.
Famous related terms
- HTTP/3 —
HTTP/3 = HTTP semantics + QUIC— same HTTP you know, carried over QUIC instead of TCP+TLS. - TCP —
TCP = reliable + ordered + single byte stream— the protocol QUIC is replacing for browser traffic, still the right tool for many other workloads. - TLS 1.3 —
TLS 1.3 ≈ "the fast modern handshake"— QUIC reuses its key exchange. - Head-of-line blocking —
HoL blocking ≈ "one slow car blocks the whole lane"— the specific failure mode QUIC’s per-stream design fixes. - Protocol ossification —
ossification ≈ "you can't change it because the middle of the network won't let you"— the structural reason a TCP successor had to be built on UDP.
Going deeper
- RFC 9000 — the primary source for “what is a QUIC stream, a connection ID, a packet number space,” and the only place the answers are normative rather than paraphrased.
- “The QUIC Transport Protocol: Design and Internet-Scale Deployment” (Langley et al., SIGCOMM 2017) — read this for the empirical question: what actually improved when Google ran gQUIC at scale, and by how much?
- Cloudflare’s HTTP/3 and QUIC write-ups — the explainer for “how do these pieces fit together end to end,” pitched well below the RFC’s reading level.
qlog/qvis— the rabbit hole for “how do I even debug this now thattcpdumpshows me encrypted noise?”