WebSocket Transport Parity Under Load
The Context
FabricMMOGTransportPeer connects via WebTransport (QUIC) first and transitions to STATE_TRYING_WS if WebTransport is unavailable. Over WebSocket (TCP), CH_INTEREST and CH_PLAYER lose their unreliable semantics: stale entity snapshots queue rather than being discarded. The frame_channels = true mode is required over WebSocket to multiplex logical channels over a single TCP stream. Desktop clients and iOS Safari clients use this path.
The Problem Statement
The back-pressure behaviour of the WebSocket path under network congestion is undocumented and untested at simulation scale. Queued stale snapshots consume bandwidth and introduce head-of-line blocking. The magnitude of the latency difference between the WT and WS paths at p95 under realistic conditions is unknown.
Describe how your proposal will work with code, pseudo-code, mock-ups, or diagrams
Define a reproducible load test:
Environment - 100 simulated clients, 50 on WebTransport (control), 50 on WebSocket (test) - Zone with 1,800 entity slots at 50% occupancy (900 active entities) - Network condition: 20% added packet delay (uniform 50 ms), 1% packet loss (WT only; TCP masks loss) - Tick rate: 20 Hz (default), snapshot broadcast per tick
Metrics
| Metric | Measurement method |
|---|---|
| Round-trip time (p50, p95, p99) | Echo packet timestamps per client |
| CH_INTEREST queue depth | Zone server counter, sampled per tick |
| Snapshot staleness | Age of the most recent applied snapshot per client |
| Session survival at 5 minutes | Count of clients still connected |
Acceptance threshold
WebSocket p95 round-trip ≤ 2× WebTransport p95 round-trip at steady state.
If threshold is not met
Implement snapshot dropping at the zone server before enqueue on TCP connections: if a newer snapshot for the same entity is already queued for a peer, discard the older one. This restores the “drop stale” semantic that QUIC datagrams provide natively.
// In FabricMMOGZone::_broadcast_interest_snapshot()
if (_ws_peer_snapshot_queued[p_peer_id][entity_id]) {
_dequeue_entity_snapshot(p_peer_id, entity_id); // drop stale
}
_enqueue_entity_snapshot(p_peer_id, entity_id, snapshot);
_ws_peer_snapshot_queued[p_peer_id][entity_id] = true;Write results to manuals/decisions/attachments/websocket-parity-test-results.csv. If the threshold is met, the WS path is declared production-ready for the primary desktop audience. If not, the dropping mechanism above is merged before the WS path is promoted.
The Benefits
The test produces a quantified answer to an open question about the primary client path. If the threshold is met, no code change is required. If not, the dropping fix is small and targeted.
The Downsides
Simulated clients do not replicate real heterogeneous network conditions. The 2× threshold is a judgement call; a stricter threshold (1.5×) would require the dropping mechanism even if the current path performs acceptably in practice.
The Road Not Taken
Switching the WS path to WebRTC data channels would restore unreliable semantics over UDP without requiring QUIC. WebRTC introduces ICE negotiation complexity and is not available in all WebSocket-only environments (e.g. restricted corporate networks where only TCP/443 is open). The frame_channels TCP path is more universally available.
The Infrequent Use Case
A client on a very low-bandwidth link (mobile data, throttled) will see queue growth regardless of transport. Rate-limiting snapshot frequency per-peer (adaptive tick rate) is a separate mitigation not covered here.
In Core and Done by Us
- Load test harness:
multiplayer-fabric-zone-console/bench/ws_parity_test.exs - Results file:
manuals/decisions/attachments/websocket-parity-test-results.csv - Conditional: snapshot dropping in
fabric_mmog_zone.cppif threshold not met
Status
Status: Proposed
Decision Makers
- iFire
Further Reading
20260421-webtransport-zone-transport.md— base transport designmultiplayer-fabric-godot/modules/multiplayer_fabric_mmog/fabric_mmog_transport_peer.h— STATE_TRYING_WSmultiplayer-fabric-godot/modules/multiplayer_fabric/fabric_multiplayer_peer.h— frame_channels mode