backend / protocols / websockets / 03_websocket_vs_alternatives.md

WebSocket vs Alternatives

7 interview angles 7 min read source

WebSocket vs Alternatives

For “real-time” communication, four common options. Each fits different access patterns. Picking the right one is more important than knowing which is “fastest.”

The choices

Tech Direction Best for
HTTP polling client pulls, on schedule rare updates, simple infra
Long polling client pulls, server holds until update rare updates, no WS support
Server-Sent Events (SSE) server → client only, over HTTP server-pushed updates, no client→server stream
WebSocket bidirectional, over HTTP-upgraded TCP chat, collab, games, fully duplex
gRPC streaming bidirectional, over HTTP/2 service-to-service real-time
WebRTC data channels bidirectional, peer-to-peer low-latency, P2P
HTTP/2 server push mostly defunct (removed from Chrome 2022)

For browser clients: SSE for server → client; WebSocket for bidirectional; long-polling as fallback when neither is supported.

For service-to-service: gRPC streaming. WebSocket works but isn’t idiomatic.

HTTP polling

setInterval(() => fetch("/api/messages").then(...), 5000);

Client asks every N seconds. Server responds with current state.

Pros: trivial, works everywhere, no special infra. Cons: latency = polling interval; wasteful (most polls return nothing new); doesn’t scale to many clients × short intervals.

Use when: updates are minute-scale or rarer, low client count.

Long polling

async function poll() {
  while (true) {
    const response = await fetch("/api/messages?since=" + lastId);
    if (response.ok) processUpdates(await response.json());
  }
}

Client makes a request; server holds it open until there’s an update (or a timeout, typically 30s). Returns. Client immediately polls again.

Pros: near-real-time delivery; works through any HTTP infra. Cons: each “session” is many requests; complex server-side (need long-held connections); server resources tied up per client.

Use when: WebSocket / SSE is blocked by infrastructure; you need real-time on browsers that don’t support them (very rare now).

Server-Sent Events (SSE)

const es = new EventSource("/api/events");
es.onmessage = (e) => console.log(e.data);

Server keeps an HTTP response open; sends events as data: ...\n\n chunks.

# FastAPI
from fastapi.responses import StreamingResponse

async def event_stream():
    while True:
        msg = await get_next_message()
        yield f"data: {msg}\n\n"

@app.get("/events")
def events():
    return StreamingResponse(event_stream(), media_type="text/event-stream")

Pros:

  • Native HTTP — no protocol upgrade. Works through proxies, firewalls, HTTP/2.
  • Built-in browser auto-reconnect (with Last-Event-ID resume).
  • Simple text protocol; easy to debug with curl.
  • Each event is a discrete message — no framing concerns.

Cons:

  • One-way (server → client only). Client posts via separate HTTP requests.
  • Text-only (binary is awkward; base64 it).
  • Connection limit per origin in browsers (6 historically; not an issue with HTTP/2 multiplexing).
  • nginx default buffering blocks SSE — set proxy_buffering off.

For dashboards, notifications, server-pushed updates with rare client → server messaging: SSE is simpler than WebSocket.

For nginx + SSE see ../nginx/10_websocket_proxying.md.

WebSocket

True bidirectional. Either side can send anytime.

See 01_websocket_protocol.md.

Pros:

  • Both directions equal.
  • Low overhead per message (small frame headers).
  • Binary or text.
  • Subprotocols + extensions.

Cons:

  • Not HTTP after handshake — caches / WAFs / proxies that don’t speak WS misbehave.
  • Stateful; pinned to one server (load balancing complications).
  • Browsers can’t set custom headers on the handshake (only via subprotocol negotiation hack).
  • More moving parts than SSE.

Use when: client → server real-time (chat input, collab editing) plus server → client. Or genuinely bidirectional protocols.

gRPC streaming

service Chat {
  rpc Connect(stream ClientMessage) returns (stream ServerMessage);
}

HTTP/2-based; binary Protobuf; first-class streaming (server, client, or bidirectional).

Pros:

  • Typed schema.
  • Binary efficient.
  • HTTP/2 multiplexing.
  • Code generation in many languages.

Cons:

  • No native browser support (needs gRPC-Web with a translating proxy).
  • Less debuggable (binary).
  • Tooling overhead.

For service-to-service real-time: gRPC streaming wins. For browser real-time: WebSocket (or SSE).

See ../grpc/03_service_types_streaming.md.

WebRTC data channels

Peer-to-peer, after a signaling phase to establish connection.

const pc = new RTCPeerConnection();
const dc = pc.createDataChannel("game");
dc.onmessage = (e) => console.log(e.data);
dc.send("hi");

Pros:

  • Direct peer-to-peer (no server in the data path after setup).
  • Low latency (UDP-based via SCTP).
  • Ordered / unordered, reliable / unreliable per message (configurable).

Cons:

  • Complex setup (signaling server, STUN/TURN servers for NAT traversal).
  • NAT issues mean ~30% of clients need a relay (TURN).
  • Steep learning curve.

Use when: real-time gaming, video/audio sidechannel, P2P file transfer. Overkill for chat.

Comparison table

Polling Long-poll SSE WebSocket gRPC stream WebRTC
Browser native yes yes yes yes no (gRPC-Web) yes
Bidirectional n/a request/response no (server → client) yes yes yes
Transport HTTP HTTP HTTP HTTP/1 upgraded HTTP/2 UDP (SCTP)
Setup latency per-poll RTT per-poll RTT 1 RTT 1 RTT + handshake 1 RTT many RTTs (signaling)
Per-message overhead full HTTP full HTTP tiny (“data: …\n\n”) few bytes (frame header) small binary very small
Server resource per request held connections held connections held connections held connections none (P2P)
Reconnection trivial built into protocol built in (browser) manual manual manual
Through proxies universal universal usually sometimes problematic HTTP/2-aware proxies NAT traversal needed

Decision tree

Need real-time updates?
├── Server → client only?
│   └── SSE
├── Bidirectional, browser client?
│   └── WebSocket
├── Bidirectional, service-to-service?
│   └── gRPC streaming
├── P2P, low-latency, gaming/video?
│   └── WebRTC
└── Updates are infrequent (minutes)?
    └── Polling (simpler)

When to choose SSE over WebSocket

  • Mostly server → client: dashboards, notifications, live feeds, AI streaming responses (ChatGPT-style token streaming uses SSE under the hood).
  • Want HTTP semantics: caching, HTTP/2 multiplexing, simpler proxy story.
  • Easy reconnection: browser auto-reconnects with Last-Event-ID resume.
  • No need for client → server stream: separate HTTP POSTs for client → server.

A lot of “WebSocket” use cases are actually SSE candidates. Streaming LLM responses, for instance, is server → client only.

When to choose WebSocket over SSE

  • True bidirectional: chat (typing indicators going server → client AND client → server at unpredictable timing), collaborative editing (operational transform messages flow both ways).
  • Binary data efficiently: SSE forces base64 encoding.
  • High message rate: WebSocket frame overhead is smaller than SSE’s data: ...\n\n line headers (though usually marginal).
  • Want to send custom headers from client: actually, neither lets you set arbitrary headers from browser JS — but WebSocket subprotocol can carry version info.

When to choose gRPC streaming

Service-to-service. Not browser-facing.

Common interview confusions

  • “WebSocket is always faster than SSE.” — both add <1ms over a TCP connection once established. The “fast” decision is what fits your access pattern, not protocol overhead.
  • “Long polling is obsolete.” — still used as fallback in libraries (Socket.IO, some PaaS chat services) for restrictive networks.
  • “HTTP/2 server push replaces SSE.” — server push was largely removed from Chrome in 2022. SSE remains the right answer for server → client over HTTP.

Interview angle

  • “Compare WebSocket, SSE, and long-polling.” — WebSocket: bidirectional, persistent, after HTTP upgrade. SSE: server → client over plain HTTP, browser auto-reconnects. Long-polling: client repeatedly issues HTTP requests; server holds until update. Pick by direction + infra constraints.
  • “When would you use SSE instead of WebSocket?” — server → client only updates (dashboards, notifications, streaming LLM responses); want plain HTTP semantics (HTTP/2 multiplexing, easier through proxies); want built-in browser auto-reconnect; client → server can be plain HTTP POSTs.
  • “Why not always use WebSocket?” — stateful (load balancing complications), not HTTP after upgrade (caching/proxying issues), no built-in reconnection (need to implement), connection pinned to one server.
  • “What’s the difference between WebSocket and HTTP polling?” — polling: client repeatedly asks, server responds with current state. WebSocket: persistent connection, server pushes when updates happen. Polling latency = interval; WebSocket latency = network RTT.
  • “WebSocket vs gRPC streaming?” — WebSocket for browser clients (gRPC isn’t browser-native). gRPC streaming for service-to-service (typed, binary, multiplexed over HTTP/2).
  • “What’s the canonical use case for WebRTC data channels?” — low-latency P2P (gaming, real-time collaboration where you can tolerate the setup complexity). Overkill for chat. Requires signaling server + STUN/TURN.
  • “You’re building a stock-ticker page that streams prices. WebSocket or SSE?” — SSE if it’s only server → client (price updates). WebSocket if users also send orders / commands over the same channel. Many stock tickers use SSE.