Backend / Protocols / WebSockets / 03_websocket_vs_alternatives.md

WebSocket vs Alternatives

Updated 7 interview angles 9 min read source
On this page17
  1. The choices
  2. The mechanism: what each costs on the wire
  3. HTTP polling
  4. Long polling
  5. Server-Sent Events (SSE)
  6. WebSocket
  7. gRPC streaming
  8. WebRTC data channels
  9. Comparison table
  10. Decision tree
  11. When to choose SSE over WebSocket
  12. When to choose WebSocket over SSE
  13. When to choose gRPC streaming
  14. Where it breaks in production
  15. The trade not taken: WebSocket for a stream that only goes one way
  16. Related
  17. Interview angle

WebSocket vs Alternatives

Article.

For “real-time” communication there are four common options, and the interview question is not which is fastest. It is which one fits the direction and rate of your messages, and what each costs once you have ten thousand connections behind proxies and load balancers.

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.

The mechanism: what each costs on the wire

Polling pays for a whole HTTP exchange per check; SSE keeps one response open and writes text events into it; WebSocket upgrades the connection and sends small binary frames. Serialized with real protocol libraries (h11 0.16, wsproto 1.3.2) for one 32-byte price update:

text
payload 32 bytes
  poll (request + response):   434 bytes
  SSE event:                   47 bytes
  WebSocket, server to client: 34 bytes
  WebSocket, client to server: 38 bytes (masked)
10,000 clients polling every 5 s: 2,000 requests/s, 0.9 MB/s,
  whether or not prices changed

Polling’s cost is headers and requests that find nothing new, paid on a schedule. SSE and WebSocket are within a few bytes of each other per message: the SSE id: and data: lines against a 2-byte frame header, plus a 4-byte mask on frames a browser sends. So the choice between those two is about direction and infrastructure, not bytes.

The other cost is per connection: both hold a socket and server memory per client for as long as it is connected, which is what the scaling section below is about.

HTTP polling

javascript
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

javascript
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)

javascript
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.

python
# 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 WebSocket Proxying.

WebSocket

True bidirectional. Either side can send anytime.

See The WebSocket protocol.

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

protobuf
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 Service Types — Unary and Streaming.

WebRTC data channels

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

javascript
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

The HTTP-based options, which is the choice most services actually face:

Polling Long-poll SSE
Direction request/response request/response server to client
Setup per-poll RTT per-poll RTT 1 RTT
Per message full HTTP full HTTP tiny
Server cost per request held connection held connection
Reconnect trivial in the protocol built into browser
Proxies universal universal usually fine

The persistent-connection options:

WebSocket gRPC stream WebRTC
Direction bidirectional bidirectional bidirectional
Transport HTTP/1 upgrade HTTP/2 UDP (SCTP)
Setup 1 RTT + handshake 1 RTT many, signalling
Per message frame header small binary very small
Server cost held connection held connection none, peer to peer
Reconnect manual manual manual
Browser native via gRPC-Web native
Proxies sometimes awkward needs HTTP/2 aware NAT traversal

Decision tree

text
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.

Where it breaks in production

  • A proxy buffers the stream. nginx buffers upstream responses by default, so SSE events arrive in bursts or only when the buffer fills. Send X-Accel-Buffering: no from the app or turn off proxy_buffering for the route, and disable response compression on it (Server-Sent Events (SSE)).
  • Idle timeouts kill quiet connections. A load balancer closes a connection with no traffic after its idle timeout (60 s by default on an AWS ALB). Send a heartbeat well inside it: an SSE comment line (:\n\n) or a WebSocket ping every 15-30 s.
  • Six connections per origin. Over HTTP/1.1 a browser opens at most six connections per origin, and each SSE stream holds one: the seventh tab, or the page’s own API calls, stall. HTTP/2 multiplexes streams on one connection and removes the limit.
  • Deploys cause a reconnect storm. Every WebSocket on a draining instance reconnects at once; without jittered backoff they arrive together and overload the remaining instances. Drain gradually and add jitter (WebSocket Connection Management).
  • Cross-site WebSocket hijacking. Browsers do not apply CORS to the WebSocket handshake and send cookies with it, so a page on another site can open an authenticated socket. Check the Origin header on the upgrade, or authenticate with a token instead of a cookie (WebSocket and SSE Authentication).
  • Fan-out across instances. A message for a user must reach the instance holding their connection; that needs a pub/sub layer such as Redis (Scaling WebSocket Applications).

The trade not taken: WebSocket for a stream that only goes one way

The common over-reach is a WebSocket for a server-to-client stream: notifications, a dashboard, an LLM answer streaming token by token. It buys a bidirectional channel the client does not use, and costs what HTTP gave for free: SSE reconnects automatically and resumes with Last-Event-ID, authenticates like any request, passes through HTTP middleware, logging and HTTP/2 multiplexing, and needs no upgrade handling at the load balancer.

The client’s occasional messages (send a chat line, cancel a generation) are ordinary POSTs. WebSocket earns its place when the client also streams: typing indicators, cursor positions, game input. Two other answers worth knowing: HTTP/2 server push is not an alternative (Chrome removed it in 2022), and long polling is not obsolete, it is the fallback libraries such as Socket.IO keep for networks that block both.

Interview angle 7

  • “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.
    • Follow-up: is WebSocket cheaper per message? Barely: 34 bytes against SSE’s 47 for a 32-byte update, while a poll cost 434. The choice is direction and infrastructure, not bytes.
    • Follow-up: your SSE events arrive in bursts behind nginx. Why? Proxy buffering; send X-Accel-Buffering: no or turn buffering off for the route, and keep a heartbeat inside the load balancer’s idle timeout.
  • “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.