WebSockets and SSE
Protocols for real-time communication between browser and server, without the client needing to request data constantly. WebSocket for bidirectional flow; SSE for simple unidirectional push; Long Polling as a universal fallback.
Intent
Classic HTTP is request-response: the client asks, the server answers, and the connection closes. For real time — chat, games, notifications, live dashboards — this model requires continuous polling, which wastes bandwidth and increases latency. WebSocket, SSE, and Long Polling eliminate that waste in different ways.
The choice between the three depends mainly on two dimensions: the direction of data flow (bidirectional or server→client only) and the infrastructure requirements (corporate proxies, load balancers, whether HTTP/2 is supported).
Problem
Classic HTTP was designed for the "web document" model: the browser requests a resource, the server delivers it, and the connection closes. That's efficient for static pages, but creates three problems for real-time applications:
- Polling latency: if the browser checks for new data every second, the effective latency is up to 1 second even when the server has data immediately. Reducing the interval increases network cost.
- Per-request overhead: every HTTP request carries headers of tens to hundreds of bytes (cookies, User-Agent, Accept, etc.). In a chat application with 100 messages per minute, this overhead far exceeds the useful content.
- The server can't initiate communication: in the classic model, the server can only respond, never notify. For "you received a message" or "your order was executed," the client has to keep asking.
How it works
WebSocket — persistent full-duplex connection
WebSocket starts with an HTTP handshake: the client sends a
request with Upgrade: websocket and the server
responds with 101 Switching Protocols. From then
on, the TCP connection stays open and both sides can send
frames in either direction, at any time,
without per-message HTTP header overhead.
// HTTP handshake → ws://
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
// From here on: binary or text frames in both directions
// with no HTTP overhead per message
The WebSocket frame has minimal overhead: 2–10 bytes of header versus 200–800 bytes of HTTP headers. In high-frequency applications (trading, games), that difference is significant.
Scalability: WebSocket connections are stateful. The same client must always reach the same server instance (sticky session), or the server must use an external broker (Redis Pub/Sub, Kafka) to propagate messages between instances — broadcasting only works within a single instance without external coordination.
SSE — Server-Sent Events
SSE uses plain HTTP: the client makes a GET and
the server responds with Content-Type: text/event-stream,
keeping the connection open and sending events in the format
data: ...\n\n as they arise. It's
unidirectional: only the server sends data;
the client uses the browser's native EventSource
API.
// Server sends events in SSE format
data: {"type":"price","value":142.5}\n\n
data: {"type":"price","value":143.1}\n\n
: keepalive comment (avoids proxy timeouts)\n\n
data: {"type":"alert","msg":"target reached"}\n\n
Advantages of SSE over WebSocket for unidirectional cases:
-
Automatic reconnection: if the connection
drops, the browser reopens it automatically and sends the
last received ID via
Last-Event-ID, allowing the server to resume from the correct point. - HTTP/2 multiplexed: over HTTP/2, multiple SSE streams share a single TCP connection, eliminating HTTP/1.1's 6-parallel-connection limit.
- Simpler to implement and debug: it's plain HTTP, visible in the browser's network tools, compatible with any proxy, and with no special handshake.
Long Polling — server push via repeated pull
Long Polling simulates server push without a specialized protocol: the browser makes a normal HTTP request, but the server doesn't respond immediately — it holds the connection open until it has data to send (or until a configured timeout). When it responds, the browser immediately opens a new request to wait for the next update.
It's the approach with the highest overhead (full HTTP headers every cycle) and highest latency (latency = server response time + time to establish a new request), but it works through any corporate proxy and any restricted network environment, which makes it a reliable fallback.
Comparison table
WebSocket SSE Long Polling
────────────────────────────────────────────────────────────────
Protocol ws:// / wss:// HTTP HTTP
Direction Full-duplex Server → client Server → client
Overhead/msg 2–10 bytes ~50 bytes 200–800 bytes
Auto reconnect No (manual) Yes (native) No (manual)
HTTP/2 Not compatible Compatible Compatible
Corp. proxies May block Usually OK Always works
Complexity Medium Low Low
────────────────────────────────────────────────────────────────
Ideal for Chat, games, Notifications, Fallback,
collaboration, feeds, progress, restricted
trading, IoT live dashboards corporate envs
WebSocket scalability with multiple instances
WebSocket creates stateful connections: if user A is connected to instance 1 and user B is on instance 2, a message from user B to user A doesn't arrive by default — instance 1's server doesn't know instance 2 has something to deliver.
The two standard solutions:
- Sticky session: the load balancer guarantees the same client always reaches the same instance. Simple, but reduces load balancing effectiveness and complicates zero-downtime deploys.
- External Pub/Sub: each instance publishes messages to a broker (Redis Pub/Sub, Kafka, NATS) and subscribes to the topics of its connected clients. Any instance can receive a message and deliver it to the correct client. More robust and scalable, but more complex to operate.
When to use each protocol
- WebSocket: when there's genuinely bidirectional real-time communication — chat, collaborative editing, multiplayer games, trading dashboards where the client also sends data frequently (cursor position, trading commands). Minimal latency is essential.
- SSE: when the server needs to send updates to the client but the client doesn't continuously send data back. Notifications, activity feeds, job progress bars, monitoring dashboards. Prefer SSE over WebSocket in these cases — it's simpler and takes advantage of HTTP/2.
- Long Polling: use as a fallback when SSE doesn't work (corporate proxies that buffer responses, environments with old support). Many libraries (Socket.IO, SignalR) implement Long Polling as an automatic fallback.
When to avoid
- WebSocket when SSE would suffice: WebSocket has more operational overhead, requires sticky sessions or pub/sub, and doesn't take advantage of HTTP/2. For unidirectional flow, SSE is the simpler, more robust choice.
- WebSocket for 1M+ simultaneous connections without a dedicated architecture: each connection keeps state on the server. At high scale, this requires dedicated gateway servers (like Pushpin, Fanout, or specific event-driven architectures), not conventional application servers.
Pros and cons
WebSocket — Pros
- Real full-duplex: both sides send data without waiting for the other to finish.
- Minimal overhead per message (2–10 bytes vs. 200+ for HTTP headers).
- Lower latency than any HTTP-based solution.
- Natively supported in all modern browsers and most frameworks.
WebSocket — Cons
- Stateful: requires sticky sessions or external pub/sub for multiple instances.
- Not compatible with HTTP/2 multiplexing.
- Some corporate proxies block or inspect ws:// — always use wss://.
- Reconnection needs to be implemented manually on the client.
SSE — Pros
- Plain HTTP: transparent to proxies, load balancers, and network tools.
- Native automatic reconnection via EventSource + Last-Event-ID.
- Takes advantage of HTTP/2: multiple streams on a single TCP connection.
- Simpler to implement and debug than WebSocket.
SSE — Cons
- Unidirectional: the client can't send data over the SSE stream — needs separate HTTP requests.
- Some old proxies buffer the response, breaking streaming (mitigable with keepalive comments).
Common pitfalls
1. WebSocket without sticky session and without pub/sub
The most common mistake in horizontal deploys: user A connects to instance 1, user B connects to instance 2, and when B sends a message to A, instance 2 tries to deliver it — but A isn't connected there. The message is silently lost. The symptom is "sometimes messages don't arrive," and it gets worse exactly when there's more traffic (and therefore more instances).
Solution: before scaling horizontally, implement Redis Pub/Sub or equivalent. Each instance publishes messages to the recipient's channel, and every instance subscribes to all the channels of its connected clients.
2. ws:// without TLS in production
ws:// connections (without TLS) are transparent to
intermediary proxies, which can inject content, modify frames,
or simply block the connection. In production, always use
wss:// (WebSocket over TLS), which is treated as
HTTPS by proxies.
3. SSE blocked by corporate proxies
Some corporate proxies buffer HTTP responses before passing
them to the client, which breaks SSE streaming (the client
never receives intermediate events, only the final one). The
mitigation is to send periodic comments
(: keepalive\n\n) every 15–30 seconds, which force
a flush in many proxies. If that doesn't solve it, Long Polling
is the fallback.
4. Underestimated connection scale
10,000 simultaneous WebSocket connections are perfectly
manageable on a well-configured node.js server. 1,000,000
connections is a completely different architecture: it requires
dedicated gateway servers, kernel tuning (ulimit,
net.core.somaxconn), and possibly managed services
(AWS API Gateway WebSocket, Ably, Pusher). Don't treat 10k and
1M as the same problem.
Related architectures and patterns
Message Queues complement WebSocket at the backend layer: the websocket server receives messages from clients, publishes them to a queue, and processes them asynchronously, decoupling the connection layer from the processing layer.
Load Balancing is directly affected by the protocol choice: WebSocket requires sticky sessions or balancing algorithms that respect session affinity (IP hash, cookie-based), while SSE works with any balancing algorithm, including pure round-robin.
REST vs GraphQL vs gRPC covers synchronous request-response protocols. WebSocket and SSE are complementary to them — many architectures use REST for CRUD operations and WebSocket/SSE for real-time updates.