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-IDresume). - 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.
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-IDresume. - 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\nline 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.