I wanted to actually see the TCP vs UDP tradeoff instead of just reading another diagram explaining it, so I built a small tool that downloads the same file over a real TCP connection and a real UDP-based one (WebTransport, QUIC's browser API) and shows live throughput, packet loss, and an integrity check computed from the actual bytes received. No simulated numbers anywhere — if a transfer reports 0% loss, that's because nothing was actually lost.
The first version I shipped had a bug that took me a while to even notice, because it didn't look like a bug — it looked like "yeah, TCP's just faster here."
The unfair race
Every click on the UDP button opened a brand new WebTransport session: TLS handshake, QUIC handshake, all of it, from scratch. Every click on the TCP button just did a fetch() — and the browser already had a warm, already-established TCP connection to the page's own origin, because it needed one to load the page in the first place.
So TCP wasn't winning because TCP is faster. TCP was winning because it got to skip the part of the race UDP was forced to run every single time. On a small file, that one-time setup cost can be a bigger chunk of total time than the actual transfer.
The fix was to stop treating "open a connection" and "run a transfer" as one step. Now both protocols open one persistent connection when the page loads, and every button click reuses it. That's the same thing the browser was already doing for TCP by default — UDP just needed the same courtesy explicitly, since WebTransport doesn't get to freeload on an existing page connection the way HTTP does.
Once I fixed that, the comparison actually started measuring what it claimed to measure.
What "no simulation" costs you
I made a rule for myself early on: nothing on this site is allowed to be fake, ever, even when the truth is unglamorous or annoying to build. That rule turned out to be a much bigger constraint than I expected, in a good way — it forced me to actually solve problems I could have hand-waved past.
The one that bit hardest: unreliable, unordered delivery means a datagram from a transfer you already abandoned can still show up after you've started a new one. If a visitor changes the file size mid-download, the old transfer's packets don't know that — some are still in flight, and UDP doesn't take them back.
The fix is a one-byte generation tag the client chooses and sends when it starts a transfer; the server just echoes it back on every packet it sends for that transfer. Anything that arrives tagged with a stale generation gets silently dropped instead of corrupting the new transfer's byte count. It's a small piece of protocol, but skipping it would have meant occasionally reporting impossible numbers — bytes received that don't belong to anything real — which is exactly the kind of thing "no simulation" isn't allowed to paper over.
Real APIs, not what search results say about them
While building this I hit a few claims about the WebTransport API — a congestionControl hint, a getStats() method — that multiple write-ups mentioned as available. Neither exists on WebTransport.prototype in a current Chromium build; I checked by literally enumerating the prototype's own property names in a real browser instead of trusting the summary. I'd built against both, then had to revert them once I actually looked. Worth remembering that "AI Overview says X supports Y" and "X actually supports Y" are two different claims, and only one of them is checkable in five seconds in devtools.
The failure mode nobody mentions
WebTransport needs outbound UDP, and a lot of corporate and hotel networks block that outright with no fallback — the connection just fails closed. There's no universal workaround short of the network operator opening the port; a browser tab can't transparently smuggle QUIC over a blocked path the way some VPN protocols try to. If you're testing this from behind strict corporate Wi-Fi and the UDP side won't connect at all, that's very likely why, not a bug in the demo.
Seeing it
If you want to see what an honest TCP vs UDP race actually looks like instead of a diagram — including the boring cases where they perform almost identically, and the cases where they don't — it's live at tcp-vs-udp.com.