WebSockets and Server-Sent Events
Request/response has one structural limit: the server can only ever answer; it can never speak first. The client asks, the server replies, and the channel closes — so "the server needs to tell the client something now" has no native expression. Polling fakes it by asking over and over, wasting requests and adding latency. Two real mechanisms solve it properly: WebSockets and Server-Sent Events.
WebSockets upgrade an HTTP connection into a full-duplex, bidirectional channel where either side sends at any time. Server-Sent Events keep it simple: a one-way, server-to-client stream over an ordinary HTTP response, with the browser handling reconnection for you. They solve overlapping problems with very different complexity and infrastructure costs, and picking the heavier one when the lighter fits is the most common mistake here.
The WebSocket Upgrade
A WebSocket starts life as a normal HTTP/1.1 request carrying Upgrade: websocket and a Sec-WebSocket-Key header. If the server agrees, it replies 101 Switching Protocols, and from that moment the TCP connection stops speaking HTTP and starts speaking the WebSocket frame protocol. The handshake borrows HTTP only to get through proxies and negotiate the switch; everything after is a different protocol on the same socket.
Once switched, the channel is full duplex: the client and server each send message frames whenever they want, with no request/response pairing. This is what chat, multiplayer games, and collaborative editors need — both sides push, low latency, in both directions. The cost is that you are now maintaining a stateful, long-lived connection per client, and WebSocket frames are opaque to the HTTP tooling (caches, L7 routers) that understood your ordinary requests.
# the upgrade handshake: an HTTP request that becomes not-HTTP GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # server agrees and the protocol switches: HTTP/1.1 101 Switching Protocols <- after this, WS frames, not HTTP Upgrade: websocket
Server-Sent Events
Server-Sent Events never leave HTTP. The client makes one ordinary GET with Accept: text/event-stream, and the server holds the response open and writes events into it as a stream — each event a few lines of UTF-8 text ending in a blank line. The connection stays open; the body just never finishes. It is one-way only: server to client, with no channel back except making another normal request.
SSE's quiet advantage is that the browser's EventSource handles reconnection automatically — if the stream drops, it reconnects and sends a Last-Event-ID header so the server can resume from where it left off. You write almost no client code. The limits are that it is text-only (no binary frames) and one-directional, and that browsers historically capped SSE connections per origin over HTTP/1.1 — a non-issue over HTTP/2's multiplexing. For notifications, live feeds, and progress updates, SSE is the simpler, correct choice.
Connection State and Scaling
Both mechanisms replace cheap, stateless request/response with expensive, long-lived connections, and that changes how you scale. A request/response server holds a socket for milliseconds; a WebSocket or SSE server holds one open per connected client, for hours. Ten thousand concurrent users means ten thousand open file descriptors and the memory for each, even when most are idle, so you size for concurrent connections rather than requests per second.
Pushing a message to a specific user means reaching the exact server that holds that user's connection, which reintroduces the affinity problem statelessness had removed. The common pattern is a pub/sub backplane (Redis, a message bus) that fans an outbound message to whichever server holds the target connection, so any node can originate a push without knowing where the recipient is connected. Without it, horizontal scaling of a push system simply does not work.
Proxies, Timeouts, and Load Balancers
Long-lived connections die on infrastructure built for short ones. A reverse proxy or load balancer with a 60-second idle timeout will silently cut a WebSocket or SSE stream that goes quiet — the application sees a dropped connection with no error, and unless it reconnects, the user stops getting updates. The fix is application-level heartbeats (WebSocket ping frames, SSE comment lines) that keep the connection non-idle, plus raising the proxy's idle timeout.
Load balancers add their own trap. A layer-7 balancer must be told to pass the Upgrade header through, or the WebSocket handshake never completes. And because the connection is stateful and long-lived, the balancer needs affinity or a backplane so a reconnect can reach a server that can serve it. Buffering proxies cause a subtler failure: a proxy that buffers the response body holds SSE events instead of streaming them, so the client receives nothing until the buffer flushes — which for a live feed means it appears broken.
WebSockets are full-duplex: both sides push at will over a protocol-switched socket. Pick them when the client also needs to send frequently and with low latency — chat, multiplayer games, collaborative editing — and accept the stateful connection and bespoke tooling that come with leaving HTTP.
Server-Sent Events are one-way server-to-client over plain HTTP, with automatic reconnection and resume built into the browser. Pick them for notifications, live dashboards, and progress feeds where only the server pushes. Long-polling — the client re-requests and the server holds the response until it has data — is the fallback hack for environments that block both; it works everywhere but wastes requests and adds latency. Pick the simplest that fits: SSE for push, WebSockets for two-way, long-polling only when forced.
- Reaching for WebSockets when SSE suffices. If only the server pushes — notifications, a live feed — a full-duplex protocol-switched socket is needless complexity that loses HTTP tooling and reconnection for capabilities you never use.
- Ignoring proxy idle timeouts on long-lived connections. A 60-second idle timeout on a load balancer silently cuts a quiet WebSocket or SSE stream, and without heartbeats the user just stops receiving updates with no error logged.
- Shipping no reconnect or backoff logic for WebSockets. Unlike SSE's automatic
EventSourcereconnection, raw WebSockets reconnect only if you code it; a dropped connection then stays dead until the user reloads the page. - Putting a stateful WebSocket service behind a load balancer with no affinity or backplane. A reconnect lands on a server that lacks the session, and pushes cannot reach a user connected to a different node, so the system fails to scale horizontally.
- Leaving a buffering proxy in front of SSE. The proxy holds the response body instead of streaming it, so events queue invisibly and the client receives nothing until a flush — a live feed that looks completely broken.
- Default to SSE for one-way server push and reserve WebSockets for genuinely bidirectional, low-latency traffic, so you do not pay for full duplex you never use.
- Send application-level heartbeats — WebSocket ping frames or SSE comment lines — and raise proxy idle timeouts, so quiet connections are not silently culled.
- Implement reconnect with exponential backoff for WebSockets, and lean on
EventSource's built-in reconnection andLast-Event-IDresume for SSE. - Put a pub/sub backplane (Redis, a message bus) behind any multi-server push system, so any node can deliver a message to wherever the recipient's connection lives.
- Configure layer-7 load balancers to forward the
Upgradeheader and disable response buffering on streaming routes, so the WebSocket handshake completes and SSE events flow immediately.
Knowledge Check
An app only needs to push live notifications from server to client. Why prefer SSE over WebSockets?
- It runs over plain HTTP, pushes one-way, and reconnects automatically — no unused full duplex
- It can carry binary frames that WebSockets cannot
- It gives the client its own dedicated low-latency channel to push data back upstream to the server at any time
- It avoids holding any long-lived connection open on the server
A WebSocket stream silently dies after about a minute of inactivity. What is the likely cause?
- A proxy or load balancer idle timeout cutting the quiet connection
- The
101 Switching Protocolshandshake expiring and forcing a re-upgrade - HPACK header compression resetting the stream after its table fills
- A
SameSitecookie rule blocking the connection after the first minute
Why does a push system across multiple servers usually need a pub/sub backplane?
- A recipient's connection lives on one server, so a push must be routed to that node
- It compresses outbound messages so each server sends less data per push
- It makes the long-lived connections stateless so any server can serve them
- It centrally terminates the WebSocket upgrade handshake on every connection so that the individual app servers can skip it
You got correct