Choosing a real-time web transport is not a contest for the newest protocol. It is a matching problem between application semantics and transport guarantees. A notification feed, a collaborative editor, and a low-latency media control channel differ in directionality, ordering, acceptable loss, message rate, reconnect state, and browser or infrastructure constraints.
The transport also does not finish the protocol. A reliable ordered connection can still lose application state when it disconnects between a server commit and client receipt. Automatic reconnection can deliver duplicates. A fast producer can exhaust a slow browser long before the network reports failure. Correct systems define event identity, resume behavior, queue policy, and overload handling above the connection API.
The thesis is to choose the least complex transport that satisfies the message contract, then design disconnection and pressure as normal states. WebSockets provide broad full-duplex messaging, Server-Sent Events (SSE) provide a simple resumable server-to-browser stream, and WebTransport provides independent streams plus optional unreliable datagrams where HTTP/3 support and the use case justify it.
Start with Message Semantics, Not the API
List every message class before selecting a connection. For each one, record its direction, maximum size and rate, ordering scope, whether loss is acceptable, whether duplicates are safe, and how long it remains useful. “Real time” is too vague to answer any of those questions.
An operations dashboard might have these classes:
| Message | Direction | Ordering | Loss policy | Lifetime |
|---|---|---|---|---|
| Incident opened/closed | Server to client | Per tenant | Must recover | Durable history |
| Current service status | Server to client | Latest version wins | Intermediate values may collapse | Until superseded |
| Chart sample | Server to client | Per series | Some samples may be skipped | Seconds |
| Acknowledge incident | Client to server | Per incident | Retry idempotently | Until accepted |
| Cursor position | Both directions | Per participant | Old positions may drop | Milliseconds |
The table often reveals that one connection does not need to carry everything. A server-to-browser feed plus ordinary HTTP commands can be simpler than a bidirectional socket. Conversely, rapidly interleaved collaboration messages may benefit from one full-duplex session and an application protocol that multiplexes document operations and presence.
Transport reliability is not business reliability. WebSocket messages and WebTransport stream bytes are ordered and reliable while their underlying connection is healthy, but neither proves that an event was durably recorded or applied. SSE reconnection can send a last event ID, but the server still needs retained history and a defined response when that ID is too old. Datagrams intentionally permit loss. State the application invariant independently: for example, “after snapshot version , apply each event with sequence greater than in order, ignoring duplicates.”
Avoid requiring global order when only per-entity order matters. One slow document should not block unrelated documents merely to preserve an arbitrary total sequence. Equally, do not split causally dependent messages across unordered channels and hope arrival order is favorable.
Compare SSE, WebSockets, and WebTransport
SSE uses an HTTP response with Content-Type: text/event-stream. The browser’s EventSource API receives UTF-8 text events from server to client, automatically reconnects, and supports event IDs. It fits notifications, job progress, dashboards, and change feeds when client commands can use normal HTTP. Its HTTP shape works well with familiar authentication and routing, though proxies must allow streaming and must not buffer the response.
WebSocket establishes a persistent full-duplex connection exposed as messages. It fits chat, collaborative applications, multiplayer state, and control protocols with frequent traffic in both directions. Text and binary messages are available, but resume, heartbeats, request correlation, authorization changes, and backpressure policy belong to the application. Browser WebSocket exposes bufferedAmount for queued outbound bytes but no automatic application-level flow control.
WebTransport runs over HTTP/3 and exposes reliable unidirectional or bidirectional streams plus datagrams. Ordering is preserved within a reliable stream, not globally across all streams. Datagrams may be lost or arrive out of order, which can be useful for disposable state such as a recent position. Streams integrate with the browser Streams API and its pressure signals. The cost is a more involved protocol, server and certificate requirements, HTTP/3 path dependence, and a browser-support matrix that must be verified for the product’s actual clients.
| Requirement | SSE | WebSocket | WebTransport |
|---|---|---|---|
| Server to browser only | Excellent fit | Works, with extra protocol | Works, usually unnecessary |
| Frequent two-way messages | Pair with HTTP requests | Strong fit | Strong fit |
| Browser-managed reconnect | Built in, policy still needed | No | No |
| Text event framing | Built in | Application choice | Application choice |
| Binary messages | Encode or use another path | Built in | Streams and datagrams |
| Independent ordered streams | Separate requests | Multiplex in application | Built in |
| Intentionally unreliable delivery | No | No | Datagrams |
| Backpressure surface in browser API | Indirect | Limited to observation | Streams-based |
SSE and WebSocket traverse common web infrastructure, but neither should be assumed to work through every gateway configuration. Idle timeouts, response buffering, maximum connection duration, upgrade handling, and per-origin connection policy need an end-to-end test. WebTransport has stricter path requirements because every network layer involved must support the necessary HTTP/3 behavior or an explicit alternative path must exist.
Work a Sequenced Operations Feed
Return to the operations dashboard. Updates are predominantly server-to-browser, command rate is low, every durable incident transition must be recoverable, and chart samples can be coalesced. SSE plus ordinary idempotent HTTP commands is the smallest fitting design.
The initial GET /api/dashboard returns a snapshot and its sequence:
{
"version": 18420,
"services": [{ "id": "search", "status": "degraded" }],
"openIncidents": [{ "id": "inc-77", "state": "investigating" }]
}
The client then opens /api/dashboard/events?after=18420. Each durable event receives a monotonically increasing tenant sequence and the SSE id field:
id: 18421
event: incident-updated
data: {"incidentId":"inc-77","state":"monitoring"}
id: 18422
event: service-status
data: {"serviceId":"search","status":"operational"}
The blank line terminates each event. On reconnect, EventSource can send the last observed event ID, and the explicit after cursor handles the initial handoff. The server reads retained events strictly after that cursor. The client ignores a sequence it has already applied and treats a gap as a signal to fetch a new snapshot rather than guessing missing state.
There is a race if the snapshot is read and events are subscribed separately without a shared version boundary. The event log solves it: the snapshot transaction records version 18420, and retained events after 18420 remain replayable. Opening the stream later cannot lose committed changes. If retention begins at 19000 while a client requests 18422, the server sends a typed reset-required event or a conflict response, and the client replaces state from a fresh snapshot.
Commands follow a separate path:
POST /api/incidents/inc-77/acknowledgements
Idempotency-Key: 7d55c9b8-44f2-44a2-a246-c8eb6db51c34
If-Match: "incident-version-12"
The HTTP response confirms acceptance; a subsequent stream event updates every viewer. The initiating client must tolerate seeing the event before or after the command response and must not apply it twice. This protocol makes correctness depend on versions and idempotency, not timing.
Design Reconnect as a State Machine
A disconnected client should move through explicit states such as connecting, live, backing-off, resuming, resetting, and closed. The interface can distinguish stale data from live data, and telemetry can report where connection attempts fail. A boolean connected hides too much.
For WebSocket and WebTransport, use exponential backoff with randomized delay and a cap. If attempt has base delay and cap , a full-jitter policy can choose
Reset the attempt counter only after the connection remains healthy long enough to prove recovery. Otherwise a server that accepts and immediately closes connections causes a rapid loop. Browser online events and visibility changes are hints that can prompt an attempt, not evidence that the origin is reachable.
Every reconnect must answer three questions: how the client authenticates again, where replay begins, and when local outbound work may resume. Short-lived credentials can expire during a long session. Refresh them over a bounded authenticated path and avoid tokens in query strings where logs and histories can expose them. If authorization to a tenant or channel changes, the server must re-evaluate it rather than preserving subscriptions indefinitely.
Heartbeats detect dead paths and keep infrastructure from considering a quiet connection idle. WebSocket applications commonly exchange protocol-level ping/pong on the server side or small application heartbeats when the browser API does not expose control frames. SSE servers can send comment lines such as : keepalive. Heartbeat intervals must sit comfortably below known idle timeouts without becoming material traffic, and absence should trigger the same reconnect state machine rather than a parallel recovery mechanism.
Do not replay ephemeral messages. A stale cursor position or chart animation has no value after reconnect; send the current state. Durable operations need a retained log or snapshot. Mixing both in one undifferentiated queue either loses important events or replays obsolete ones.
Bound Backpressure at Every Fan-Out Stage
A connection can remain open while its consumer falls behind. Pressure accumulates through the broker subscription, gateway queue, socket buffers, browser event queue, state updates, and rendering. An unbounded queue only converts temporary slowness into eventual memory exhaustion and extreme latency.
Define a bounded policy per message class:
- Durable transitions are retained in the shared event log, not indefinitely in each connection’s memory. A slow connection is closed with its last acknowledged or sent cursor and resumes later.
- Latest-state updates are coalesced by key. Ten pending status changes for one service can become its newest version when intermediate values have no required meaning.
- Disposable samples can be sampled or dropped under pressure, with a counter that makes loss visible.
- Commands are admission-controlled before execution and receive explicit overload errors rather than silently queuing without a deadline.
For browser WebSocket sends, inspect bufferedAmount before adding more data and stop producing or coalesce when it exceeds an application threshold. The value indicates bytes queued by the browser; it is not an acknowledgement that the peer processed earlier messages. Resume only after observing that the queue drains, usually through a scheduled check because the standard API has no drain event.
WebTransport writers expose stream pressure through writer readiness and desired size. Honor those signals, but retain application limits: a stream can exert backpressure while a producer continues accumulating objects elsewhere. Datagrams may also face queue limits and transport loss. Dropping is acceptable only for a class whose semantics explicitly permit it.
On the server, assign a per-connection byte and message budget. Serialize before enqueueing so the budget reflects actual output size. When the limit is exceeded, coalesce, drop, or close according to message class. Never let one slow subscriber block the shared publisher loop. Global admission control is still required when the number of healthy clients exceeds fan-out capacity.
Scale Connections Without Hiding State in One Process
Long-lived connections change the shape of scaling. Track active connections, subscriptions, bytes, queue depth, reconnect rate, and event lag per gateway. Autoscaling based only on request rate misses a gateway holding many quiet sessions. Draining an instance requires stopping new connections, signaling or waiting for clients to reconnect, and allowing a bounded grace period.
Sticky routing can reduce subscription churn, but it is not a correctness mechanism. The next connection may reach another process after a deploy or failure. Store durable events, cursor state where needed, and authorization in shared systems with clear retention. A broker distributes live events to gateways; the event log or snapshot store repairs gaps. Do not assume a transient pub/sub delivery is a replayable history.
Channel cardinality affects the design. Subscribing every gateway to every tenant wastes bandwidth, while one broker subscription per browser can overload broker metadata. Aggregate subscriptions per gateway and tenant or topic, then fan out locally with bounded queues. Remove the broker subscription when the final local listener leaves, accounting for connection races.
Proxies and load balancers need explicit streaming configuration. Disable inappropriate response buffering for SSE, permit WebSocket upgrade or extended connection behavior supported by the stack, set idle and maximum-duration policies knowingly, and preserve end-to-end cancellation. Compression can save bandwidth for large text messages but consumes CPU and can add latency by batching; measure it with representative payloads rather than enabling it universally.
WebTransport should have a deliberate fallback when clients or networks cannot establish it. The fallback need not preserve every capability. A product may use WebTransport datagrams for disposable presence but fall back to a WebSocket carrying a lower-rate coalesced representation. Capability negotiation belongs in the protocol and telemetry, not in optimistic assumptions about the network path.
Secure, Test, and Operate the Protocol
Authenticate the connection and authorize every subscription scope. For WebSockets, validate the Origin header and require an anti-forgery or connection credential appropriate to the deployment; browsers can initiate cross-origin WebSocket connections and may attach ambient credentials. For SSE, use same-origin hardened cookies or an explicit supported credential flow, and avoid secrets in URLs. Recheck authorization for long sessions and close subscriptions promptly after revocation.
Validate every message against a versioned schema, cap frame and event sizes, limit subscription count, and rate-limit commands. Treat decompression, parsing, and fan-out as resource-consuming work before business handling begins. Logs should include connection and event IDs without copying private message bodies.
Protocol tests should use a deterministic model: apply snapshot , deliver events with duplicates and gaps, disconnect at each boundary, reconnect from every cursor, expire retention, and assert convergence. Integration tests should add a real proxy, delayed and fragmented delivery, idle periods, credential expiry, rolling gateway drain, slow consumers, broker interruption, and multiple tabs. For unreliable datagrams, inject reordering and loss rather than testing only a clean local network.
Operational dashboards need connection attempts by outcome, handshake latency, concurrent sessions, reconnect storms, heartbeat expiry, replay distance, reset frequency, per-connection queue percentiles, dropped/coalesced messages, outbound bytes, and end-to-end event age. Alert on user impact such as growing event lag or reset loops, not merely a large connection count.
The final selection rule is intentionally conservative. Choose SSE when the server owns the stream and ordinary HTTP can carry occasional commands. Choose WebSocket when frequent bidirectional messages need broad deployment support and one ordered channel is acceptable. Choose WebTransport when independent streams or intentionally unreliable datagrams create concrete value and the client and infrastructure matrix supports it. Whichever transport wins, correctness lives in sequence numbers, idempotency, bounded queues, resume rules, and tested recovery. A connection is temporary; the application protocol must survive it.