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:
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 changedPolling’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
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 WebSocket Proxying.
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 Service Types — Unary and Streaming.
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
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
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.
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: nofrom the app or turn offproxy_bufferingfor 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
Originheader 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.
Related
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: noor 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.