For eleven days, one of our client's four dispatch offices was running on the fallback transport and nobody knew. Not degraded, not broken — the board updated, vehicles moved, orders changed state. It was simply doing all of that over the WebSocket path we had built as a safety net and expected roughly nobody to use. We found it because a dispatcher mentioned in passing that the map felt "a bit sticky compared to Tuesdays," and Tuesdays were the days that office worked from a different building.
The rollout had gone well by every metric we were collecting. That was the problem. We were collecting metrics about the system, and we had no metric that answered the only question that mattered: which transport is this particular session actually on right now.
We have now shipped WebTransport in two production products and scoped a third. The honest summary is that the protocol work — streams, datagrams, deciding what rides on which — took about two days of real thinking. The other eight weeks went into the part where it does not connect, and into finding out who that happens to.
What changed is Safari, and that is the entire story
WebTransport is not new. Chrome shipped it in 2022, Firefox followed in 2023, and for three years it sat in the category of technology you could demo but not deploy, because a transport with a browser-shaped hole in it is not a transport. Safari 26.4 closed that hole in March as part of the Interop 2026 work, and the calculus changed in a single release. Not because the API improved — it did not — but because the fallback path stopped being the primary path for a quarter of your traffic.
What you get is one connection over HTTP/3 that carries two very different things at once. Reliable, ordered streams, as many as you want, with no head-of-line blocking between them — one slow message on stream A does not stall stream B, which is the failure mode that makes a single multiplexed WebSocket feel laggy under load. And unreliable datagrams, which are fire-and-forget, may arrive out of order, and may not arrive at all.
That second channel is the actual reason to do this. Everything else is a nicer version of something you already had.
The split takes an afternoon if you ask the right question
Teams get stuck on this because they frame it as a performance question — which channel is faster — and the answer is that neither is, particularly. The useful question is different and it takes about two minutes per message type: if this arrives four hundred milliseconds late, what should happen?
For a cursor position, a vehicle's last GPS ping, or a typing indicator, the answer is that nothing should happen, because the information is already wrong by the time it lands and a newer one is right behind it. Retransmitting it is worse than dropping it. For an order moving from assigned to collected, the answer is that it must land, in order, or two people are looking at different versions of reality. Once you ask it that way the classification writes itself.
| Signal | Channel | Why |
|---|---|---|
| Order and dispatch state | Reliable stream | Every transition has to land, in order, or two offices disagree about a live job. Losing one is a correctness bug, not a visual glitch. |
| Vehicle positions, cursors, presence | Datagram | A position that arrives late is simply wrong, and a newer one is already in flight. Dropping it is the correct behaviour. |
| Client-side telemetry | Datagram | We sample it at the aggregator anyway. Paying for retransmission of data we then throw away is pure cost. |
| Auth, session join, acknowledgements | Reliable stream | Control plane. If one of these is lost the session ends up in a state neither side can name or recover from. |
“Reliable and unreliable are not quality tiers. They are a question about what should happen when a message arrives late, and for most of a live interface the honest answer is nothing at all.”
One practical constraint that is easy to miss until it bites: a datagram is not a place to put an arbitrary payload. Keep it under roughly 1,200 bytes and it survives every path we tested; go over and behaviour varies by network in ways you will discover from a user rather than from a test. That ceiling is fine for positions and presence. It is not fine for the batched update someone will inevitably try to move onto datagrams in month three because it looked faster in a benchmark.

UDP is not a guarantee, and your users are the ones who find out
WebTransport runs over QUIC, which runs over UDP. A meaningful share of the networks real people work on treat UDP on anything other than port 53 as something to be throttled, inspected, or dropped without comment. Corporate firewalls do it by policy. Some hotel and airport networks do it because whoever configured them did it once in 2016. A couple of mobile carriers do something stranger, where the connection establishes and then dies after a few minutes of idle.
None of this is exotic and all of it is invisible to you until you measure it. Here is what our first eight weeks across two products looked like, on a user base that skews toward office desktops — which is to say, the friendly case.
The second number is the one that reorganised our thinking. We had built the fallback as a legacy-browser concession, something that would shrink to nothing as Safari updates rolled out. It is not that. It is a network concession, and it does not shrink, because the networks are not going to be upgraded on anyone's roadmap. Two of those three corporate networks belong to the client's largest customers.
“A transport that works for ninety-eight percent of sessions is not a transport. It is an optimisation with a support queue attached to it.”
Which means the WebSocket path is not scaffolding to be removed next year. It is a permanent second implementation that has to stay correct, stay tested, and carry the same semantics — and the cost of that should be in the estimate at the start, not discovered in month two. If a team is not willing to own two transports indefinitely, they should not be adopting this one yet.
Reconnection is yours, and so is the edge
WebSocket did not give you reconnection either, but everyone using it in production has been sitting on a library that did. WebTransport is early enough that most teams write against the raw API, notice that the connection-closed promise resolves cleanly, and ship. Then a laptop lid closes, or a train goes into a tunnel, and the session is simply over with no error anywhere — because from the API's point of view nothing went wrong.
- Resume state, not the socket— reconnecting is the easy half. The hard half is that the client missed an unknown number of stream messages while it was away, so every reliable channel needs a sequence number and a “send me everything after N” path. Without it, reconnection silently produces a stale board that looks live.
- Fall back on a timer, not on an error— a blocked UDP path often does not fail, it hangs. We give the connection attempt three seconds, then open the WebSocket in parallel and take whichever is ready first. Waiting for a definitive failure means waiting past the point the user has already refreshed.
- Re-probe, but rarely— a session that fell back at 09:00 because of a flaky moment should not be stuck on the slower path until lunch. We retry the primary transport once, quietly, after fifteen minutes, and never again in that session. More often than that and you are generating churn to chase a difference nobody can perceive.
The infrastructure side surprised us more than the client side did. HTTP/3 has to be terminated somewhere that speaks it, and a lot of perfectly modern production edges do not. Two of the three environments we touched had a load balancer that was TCP and HTTP/2 only, so the work was not "enable a flag" but "introduce a new ingress path, with its own certificates, its own health checks, and its own on-call runbook, next to the one that already works." Budget for that conversation with whoever owns the edge before you promise a date.

If you cannot see the transport, you cannot see the problem
This is what cost us eleven days. Every dashboard we had aggregated across all sessions, so a whole office quietly sitting on the slower path moved the p95 by an amount that looked like normal variance. The system was healthy in every chart. The people using it were not.
The fix is unglamorous and should be built before the feature ships, not after: the transport in use becomes a dimension on every client-side metric you already collect, and the fallback rate becomes a first-class number with an alert on it. Not the fallback count, the rate, split by network or by tenant — a count going up is indistinguishable from growth, which is exactly why nobody looks at it twice.
We also log the reason for falling back, and that has been worth more than the rate itself. "Connection attempt timed out" and "connection established then closed after ninety seconds" are both counted as fallback and they are completely different problems — the first is a firewall, the second was, in our case, an idle timeout on an intermediary that nobody at the client knew was in the path.
What we would tell a team starting this on Monday
- Sort messages by what should happen if they arrive late, not by how fast they need to be. That single question does the whole reliable-versus-unreliable design for you.
- Treat the WebSocket path as a permanent second implementation with the same semantics and the same tests, not as scaffolding you will delete once browser support catches up. It is a network concession, and networks do not update.
- Instrument the transport as a dimension on every client metric before you ship, or you will average a broken office into a healthy-looking chart.
- Fall back on a timer, not on an error. A blocked UDP path frequently hangs rather than failing, and the user refreshes long before the connection gives up.
- Check who terminates HTTP/3 at your edge in week one. In two of three environments this was a new ingress with its own certificates, not a configuration change.
- Keep datagram payloads under about 1,200 bytes and put a comment on it, because the person who later batches an update onto that channel will not find the limit in testing.
Was it worth doing
For the live-operations board, yes, and for a specific reason that has little to do with latency numbers. Moving vehicle positions onto datagrams meant a slow or dropped position update stopped being able to delay an order state change, because they no longer share a delivery guarantee or a queue. The board got less laggy under load, but more importantly it got harder to make wrong. That is a correctness win wearing a performance costume.
For the second product — a document editor where essentially every message has to land — it was not worth it, and we said so before building anything. Everything in that product needs a reliable ordered channel. WebTransport would have given it a slightly better multiplexing story and a second transport to maintain forever. That trade is not close.
So the test we use now is narrow on purpose: do you have a meaningful volume of messages that are genuinely disposable? If yes, this is a real improvement and the datagram channel is the thing you are buying. If no, you are taking on a permanent dual-transport maintenance cost to get a nicer version of what WebSocket already does for you, and a boring transport that everyone on the team understands is worth more than an interesting one that two people do.









