WebSockets vs Server-Sent Events
“You need to push updates from a server to a browser. Compare long polling, Server-Sent Events and WebSockets: what does each cost at the network layer, what breaks with each in production, and how do you choose?”
What this tests
- The wire mechanism of each option, not just "real time"
- Direction, framing and connection-count constraints
- Proxy, load-balancer and idle-timeout interaction
- Scaling stateful connections across a fleet
Answers by level
Read the beginner answer first and notice what is missing.
Long polling is ordinary HTTP with the response delayed: the client sends a request, the server holds it until an event or a timeout (~30 s), then the client immediately sends another. Every event costs a full request/response with headers, one held connection per client, and a gap between events during which nothing can arrive. It works through every proxy and cache because it *is* HTTP, and it is the right answer for infrequent events and hostile networks.
Server-Sent Events are one long-lived HTTP response: Content-Type: text/event-stream, a streamed body of data: lines with optional id: and retry: fields. Server-to-client only, text only (UTF-8). The browser’s EventSource reconnects automatically and sends Last-Event-ID, so resume-after-drop comes for free. Because it is a normal response it passes through load balancers and TLS termination unchanged — but any proxy that *buffers* responses (nginx by default, proxy_buffering off or X-Accel-Buffering: no to fix) turns a stream into a batch, and any idle timeout (ALB 60 s) kills a quiet stream unless the server sends a comment line (: ping) every 15–30 s. Over HTTP/1.1 each stream is a connection and browsers cap ~6 per origin, so six tabs starve the seventh; over HTTP/2 streams are multiplexed and the cap disappears. See Polling vs Long Polling vs SSE vs WebSockets.
WebSockets begin as an HTTP/1.1 request with Upgrade: websocket and a Sec-WebSocket-Key; the server answers 101 Switching Protocols and from then on the same TCP connection carries a framed, bidirectional, binary-capable protocol with a 2–14-byte header per message — no HTTP headers per message, sub-millisecond client-to-server sends. The costs: every proxy and balancer in the path must understand the upgrade (nginx needs explicit Upgrade/Connection headers forwarded; classic HTTP-mode ELBs did not support it), HTTP/2 needs the extended CONNECT of RFC 8441 which is unevenly supported, there is no automatic reconnect or resume — you build heartbeats (ping/pong frames), backoff with jitter and a resume protocol yourself — and the connection is stateful, so a user’s session lives on one node. See WebSockets.
Choose by direction and frequency. Server-to-client feeds — dashboards, notifications, streaming LLM tokens, progress — are SSE: simplest, cache- and proxy-friendly, free resume. Genuinely bidirectional low-latency traffic — collaborative editing, chat with typing indicators, games, terminals — is WebSockets. Rare events or clients behind unknown proxies: long polling. Whichever you pick, the persistent connection count is the capacity unit: each is an fd and kernel buffers on the server, and a deploy that restarts the fleet reconnects *every* client at once.
Green flags · Red flags
- Describes each mechanism on the wire: held request, streamed response, upgraded framed connection
- Knows SSE is one-directional text with automatic reconnect and
Last-Event-ID - Mentions proxy buffering, idle timeouts and the Upgrade requirement
- Names the HTTP/1.1 six-connection limit and HTTP/2’s effect on it
- Explains that persistent connections shift the scaling problem to fan-out and fd count
- Picks WebSockets by default for everything
- Does not know SSE exists or thinks it needs a library
- Unaware that proxies and load balancers can break either one
- Has no plan for reconnection or for routing a message to the right node