In our previous analysis, The State of WebTransport: Great in Browsers, Unusable Behind Reverse Proxies, we examined the infrastructure bottleneck keeping the web on WebSockets. While modern browsers have supported WebTransport for years, reverse proxies and CDNs still struggle with its multi-stream routing model.
This raises a fair engineering question: Is WebTransport actually worth the infrastructure trouble? Does replacing WebSockets with WebTransport deliver measurable gains for real-world applications, or is it an incremental theoretical upgrade?
To answer this with hard data, we conducted a rigorous, deterministic benchmark comparing WebTransport (over HTTP/3 and QUIC) against traditional WebSockets (over TCP and TLS) inside Lupyd's open-source messaging engine, Firefly.
We simulated a production-scale workload of 500 members with 3 devices each (1,500 concurrent live endpoints) communicating over simulated cellular networks subjected to 30ms round-trip latency, ±5ms jitter, and 1.0% stochastic packet loss. The results were stark: WebTransport reduced p99 tail latency from 258.74 ms down to 51.23 ms (an 80.2% drop, or 5.05× faster) while slashing peak CPU utilization by 85.1%.
Is WebTransport Worth the Infrastructure Trouble?
When networking benchmarks are run on local loopback (127.0.0.1) with zero packet loss and sub-millisecond ping, WebSockets look deceptively fast. In an idealized lab environment, TCP performs well: operating systems buffer packets smoothly, and serialization overhead is low.
In the real world, web clients rarely operate on local loopback. Mobile devices connect over LTE and 5G cellular towers with fluctuating signal strength, subway tunnels, congested public Wi-Fi, and cross-region transit links. On these links:
- Packet loss rates of 0.5% to 2.0% are normal occurrences.
- Round-trip latencies fluctuate between 20ms and 60ms depending on radio conditions.
- Real-time applications multiplex high-frequency micro-RPCs (key exchanges, read receipts, delivery acknowledgments) alongside continuous message traffic.
Under these conditions, TCP's core guarantee (strictly ordered, reliable byte delivery) becomes its main liability. A single lost IP packet halts delivery of all subsequent data on that socket until the missing segment is retransmitted.
Benchmark Topology: 1,500 Endpoints & Linux Traffic Control
To evaluate how both protocols behave under realistic conditions without external internet noise, we isolated our benchmark inside a reproducible Docker environment using Linux Traffic Control (tc netem).
Figure 1: Benchmark Emulation Architecture
1,500 Endpoints
500 Members × 3 Devices
30ms delay ± 5ms jitter
1.0% stochastic packet drop
Firefly MLS + Postgres 16
<0.1ms ping • 0% internal loss
Kernel Queuing Discipline Configuration:
The benchmark runner container's virtual network interface (eth0) was configured with a 50,000-packet buffer depth to prevent buffer exhaustion during concurrent TLS handshakes:
# Simulating real-world cellular carrier network impairment on Linux
# 30ms base round-trip delay, ±5ms normal jitter, 1.0% stochastic packet drop rate
tc qdisc add dev eth0 root netem limit 50000 delay 30ms 5ms loss 1% Workload Characteristics
- Active Endpoints: 1,500 simultaneous persistent connections (500 users with phone, desktop, and web clients).
- Duration: 300 continuous seconds (5 minutes) per transport protocol.
- Traffic Cadence: Continuous 10ms message generation loop (~90 messages/second, ~27,000 messages total per run).
- Payload Sizes: Varied pseudo-random payloads (128B, 256B, 512B, 1024B) generated via
ChaCha8Rng(seed 42) ensuring identical traffic between WebSocket and WebTransport runs.
Head-to-Head Results: 30ms Latency and 1% Packet Loss
Here is the complete telemetry recorded during the 5-minute impaired network benchmark run:
| Telemetry Metric | WebSocket (TCP) | WebTransport (QUIC) | Delta / Improvement |
|---|---|---|---|
| Active Concurrent Endpoints | 1,500 | 1,500 | Equal scale |
| Sustained Duration | 300.0 s | 300.0 s | Equal |
| Total Messages Dispatched | 27,060 | 27,091 | Identical workload |
| Effective Throughput | 90.20 msgs/sec | 90.30 msgs/sec | Parity |
| RPC Latency p50 (Median) | 33.24 ms | 33.50 ms | +0.8% (hits 30ms network floor) |
| RPC Latency p90 | 37.39 ms | 37.60 ms | +0.6% (identical on good frames) |
| RPC Latency p95 | 37.97 ms | 38.13 ms | +0.4% |
| RPC Latency p99 (Tail) | 258.74 ms | 51.23 ms | -80.2% (5.05× faster!) |
| Maximum RPC Latency Peak | 535.96 ms | 227.13 ms | -57.6% lower peak |
| Latency Jitter (Std Dev) | 24.37 ms | 10.13 ms | -58.4% less jitter |
| Cross-Device Delivery p99 | 237.58 ms | 51.23 ms | -78.4% faster delivery |
| Client Peak CPU | 12.72% | 10.82% | -15.0% lower |
| Server Peak CPU | 16.97% | 14.43% | -15.0% lower |
| Client Peak Memory (RSS) | 209.1 MB | 281.9 MB | +34.8% (+72.8 MB) |
| Bandwidth Rate | 98.6 KB/s | 145.9 KB/s | +48.0% (+47.3 KB/s) |
The Anatomy of the Collapse: Why WebSocket Fails Under Loss
Notice what happened at p50, p90, and p95 versus p99:
- At the 50th percentile (median), WebSocket and WebTransport performed almost identically (~33.2ms vs ~33.5ms). When packets are not dropped, both protocols deliver data within the 30ms network delay.
- Even at p90 and p95, latency remained tightly bounded around 37ms to 38ms.
- At p99, WebSocket latency collapsed catastrophically to 258.74 ms, with individual outliers reaching 535.96 ms.
Figure 2: Head-of-Line Blocking Mechanics Under 1% Packet Drop
This explains why a 1% packet loss rate is so devastating to WebSockets. In an application like Firefly that multiplexes dozens of concurrent micro-RPCs (pre-keys, MLS commits, delivery ACKs) over a single TCP socket, dropping one packet out of a hundred freezes the entire pipeline for every concurrent request on that wire.
How WebTransport Preserves Sub-55ms Tail Latency
WebTransport achieves an 80.2% reduction in tail latency because Firefly maps each RPC to a dedicated bidirectional stream via session.open_bi():
// Inside firefly-client/src/websocket.rs: WebTransport dedicated bidirectional RPC stream
// Dedicated stream per RPC: Zero head-of-line blocking across concurrent requests
pub async fn request_webtransport(
&self,
method: &str,
payload: Vec<u8>,
) -> Result<Vec<u8>, FireflyError> {
let session = self.get_webtransport_session()?;
// 1. Open a fresh bidirectional stream over the established QUIC connection
let (mut send, mut recv) = session.open_bi().await
.map_err(|e| FireflyError::Transport(format!("open_bi failed: {e}")))?;
// 2. Frame and transmit the protobuf ClientMessage
let client_msg = ClientMessage {
request: Some(Request { method: method.to_string(), payload }),
..Default::default()
};
let mut encoded = Vec::new();
client_msg.encode_length_delimited(&mut encoded)?;
send.write_all(&encoded).await?;
send.finish().await?; // Signals clean EOF for the request stream
// 3. Receive the direct response on the same isolated stream
let response_bytes = recv.read_to_end(MAX_RPC_PAYLOAD_SIZE).await?;
let server_msg = ServerMessage::decode_length_delimited(&response_bytes[..])?;
Ok(server_msg.into_response()?)
} Compare this with the legacy WebSocket implementation:
// Legacy WebSocket: Multiplexed over a single TCP stream with request IDs
// If any single TCP packet drops, ALL in-flight requests in pending_requests stall!
pub async fn request_websocket(
&self,
method: &str,
payload: Vec<u8>,
) -> Result<Vec<u8>, FireflyError> {
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
// Register waiter in client-side pending table
self.pending_requests.lock().await.insert(request_id, tx);
// Write into the shared TCP socket stream
let msg = ClientMessage {
request_id,
request: Some(Request { method: method.to_string(), payload }),
..Default::default()
};
self.ws_write_queue.send(msg).await?;
// Wait for the server's response to be demultiplexed from the single TCP stream
rx.await.map_err(|_| FireflyError::Timeout)
} Under WebTransport, when packet loss occurs on the UDP link:
- The QUIC engine (Quinn in Firefly) detects the missing datagram and schedules a retransmission specifically for that stream ID.
- All other active bidirectional and unidirectional streams continue to read and write bytes without waiting.
- The application layer does not maintain complex pending-request sequence maps or lock-guarded in-memory write queues.
- At p99, latency stays at 51.23 ms, only ~13ms above the network jitter floor.
Baseline Local vs. Impaired Network Comparison
To isolate the exact impact of network impairment, compare our baseline local benchmark (<0.1ms ping, 0% loss) against the impaired benchmark (30ms ping, 1% loss):
| Telemetry Metric | Baseline Local WS (0% loss) | Impaired WS (1% loss, 30ms) | Baseline Local WT (0% loss) | Impaired WT (1% loss, 30ms) |
|---|---|---|---|---|
| p50 Latency | 0.81 ms | 33.24 ms | 0.73 ms | 33.50 ms |
| p95 Latency | 2.50 ms | 37.97 ms | 1.83 ms | 38.13 ms |
| p99 Tail Latency | 10.95 ms | 258.74 ms (+2,263%) | 4.52 ms | 51.23 ms (+1,033%) |
| Maximum Latency | 47.96 ms | 535.96 ms | 19.57 ms | 227.13 ms |
| Latency Jitter (Std Dev) | 1.34 ms | 24.37 ms | 0.89 ms | 10.13 ms |
| Client Peak CPU | 30.29% | 12.72% | 4.51% (-85.1%) | 10.82% (-15.0%) |
Even on local loopback with zero packet loss, WebTransport was already 58.7% faster at p99 (4.52ms vs 10.95ms) and reduced client peak CPU by 85.1% (from 30.29% down to 4.51%). Under realistic mobile network conditions, that performance gap expands into a full order-of-magnitude difference in tail predictability.
The Trade-Offs: Memory, Bandwidth, and Framing Overhead
WebTransport is not free. Engineering requires honest accounting of costs, and the benchmark revealed two concrete trade-offs:
1. Higher Memory Footprint (+34.8%)
+72.8 MBAcross 1,500 active endpoints, peak RSS memory was 281.9 MB for WebTransport vs. 209.1 MB for WebSocket. Maintaining independent stream tracking, per-stream flow control buffers, and packet acknowledgement structures in Quinn allocates more memory than a dumb TCP byte buffer.
2. Wire Bandwidth Overhead (+48.0%)
+47.3 KB/sWire bytes averaged 1,654 B/msg on WebTransport vs. 1,119 B/msg on WebSocket. On small micro-RPC payloads (128B to 256B), QUIC packet headers, UDP encapsulation, and datagram framing add noticeable per-packet byte overhead compared to bare TCP segments.
For our encrypted messaging platform, paying a ~35% memory premium and extra wire bytes is an easy trade-off when it eliminates 500ms freezes and drops p99 tail latency by 80%.
Firefly's Dual-Transport Implementation in Rust
A key design goal when introducing WebTransport into Firefly was zero breaking changes for existing applications and FFI consumers:
1-to-1 API Compatibility Contract:
FireflyWsClient::create(...)automatically selects WebTransport if the connection URL starts withhttps://,wt://, orwebtransport://. Ifws://orwss://is provided, it falls back to WebSockets.- Both transports implement identical async interfaces:
send_message(),request(),send_action(), and group MLS ratchets. - Type aliases (
pub type FireflyWtClient = FireflyWsClient;) allow intuitive naming across Rust crates, Tauri bindings, and Kotlin/Swift mobile bridges. - All 66 core client integration tests pass unmodified across both transports.
Connecting the Dots: Why Proxy Support Matters More Than Ever
These benchmark findings make the reverse proxy bottleneck described in our proxy analysis all the more urgent:
- The performance win is proven: WebTransport is not an incremental protocol tweak. Under cellular packet loss, it completely eliminates TCP head-of-line blocking stalls and keeps tail latency under 55ms.
- The client and backend code is ready: Libraries like wtransport, webtransport-go, and Firefly prove that dual-transport architectures with automatic fallback can ship today with zero breaking API changes.
- The bottleneck is purely the proxy layer: As long as Nginx, HAProxy, and Cloudflare CDN refuse to route WebTransport sessions to origin pools, production architectures are forced to stay on WebSockets or expose origin IP addresses to direct UDP floods.
WebTransport is ready, proven, and waiting. The ball is squarely in the court of reverse proxy maintainers and CDN providers.
Frequently Asked Questions
Why does WebSocket tail latency spike to 258ms under only 1% packet loss?
WebSocket runs over a single TCP stream. TCP guarantees strictly ordered delivery, meaning if a single packet is lost, the operating system kernel cannot deliver subsequent packets to the application until the missing segment is retransmitted. Over a 30ms link with jitter, retransmission delay stalls all pending concurrent messages and RPCs behind the lost packet.
How does WebTransport achieve 51ms tail latency under packet loss?
WebTransport maps each RPC and message to dedicated bidirectional or unidirectional QUIC streams. When a UDP packet for one stream drops, QUIC retransmits only that stream's data. All other concurrent streams continue to be delivered and processed immediately without head-of-line blocking.
Why did WebTransport reduce peak CPU by 85% in Firefly?
Over WebSockets, Firefly must manage a single shared connection with user-space mutexes, request ID routing tables, and in-memory queues. Under burst loads, this causes high lock and queue contention. WebTransport delegates stream scheduling and demultiplexing to the kernel and Quinn's async I/O engine, cutting peak client CPU from 30.29% to 4.51%.
What are the trade-offs of WebTransport compared to WebSocket?
WebTransport increases client memory consumption by approximately 34.8% (281.9MB vs 209.1MB across 1,500 endpoints) to maintain per-stream QUIC flow control states and buffers. It also increases wire bandwidth by ~48% on small messages due to QUIC packet framing and datagram headers.
How does Firefly maintain backward compatibility with WebSockets?
FireflyWsClient provides 100% API parity with automatic protocol detection: connecting via ws:// or wss:// selects WebSockets, while https:// or wt:// selects WebTransport. Both transports expose the exact same async methods (send_message, request, send_action), allowing zero-downtime migration without client changes.
- ✓ In an impaired network test (30ms RTT, ±5ms jitter, 1% packet loss) across 1,500 endpoints, WebTransport reduced p99 tail latency from 258.74ms to 51.23ms—an 80.2% improvement (5.05× faster).
- ✓ TCP head-of-line blocking under 1% packet loss causes WebSocket latency spikes up to 535.96ms, freezing all concurrent requests sharing the socket.
- ✓ WebTransport's dedicated bidirectional streams isolate requests; packet loss on one stream does not pause or delay concurrent streams on the same QUIC connection.
- ✓ In baseline local loopback tests, WebTransport reduced client and server peak CPU by 85.1% (from ~30-40% down to 4-6%) by eliminating user-space queue contention.
- ✓ The trade-off: WebTransport consumed 34.8% more peak memory (281.9MB vs 209.1MB) for QUIC stream flow control state, and 48% more wire bytes due to QUIC headers on small payloads.
- ✓ Firefly MLS maintains 100% 1-to-1 API compatibility between WebSockets and WebTransport with automatic protocol detection.