Guide contents

Lesson 13 of 129

WebSockets, Server-Sent Events, and Long Polling

Plain HTTP is client-initiated: the server can respond only to a request. Applications that need fresh data quickly, such as chat, notifications, live dashboards, and collaborative editing, need ways for the server to deliver updates without the user refreshing. Four techniques exist, with different costs in latency, overhead, and infrastructure.

Short polling

The client repeatedly sends a request at a fixed interval (for example every 5 seconds) and the server answers immediately, with new data or an empty response.

  • Simple, works everywhere, and uses ordinary HTTP infrastructure.
  • Latency is up to one interval. Most requests return nothing, so it wastes bandwidth and server capacity, and load grows with the number of clients times the request rate, regardless of how many updates occur.

Example: 100,000 clients polling every 5 seconds produce 100,000 / 5 = 20,000 requests per second, even if no client has an update.

Short polling suits low-frequency updates where a few seconds of delay is acceptable.

Long polling

The client sends a request, and the server holds it open until data is available or a timeout expires (commonly tens of seconds). After responding, the client immediately sends the next request.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: GET /updates (held open)
    Note over S: waits for an event
    S-->>C: 200 with event data
    C->>S: GET /updates (next request)
    Note over S: waits, or times out with an empty response
  • Near real-time delivery over plain HTTP, and it works through most proxies.
  • Each event costs a full request/response cycle with headers. A client can miss events between one response and the next request unless the server tracks position (for example with a last-event ID or cursor). Servers hold one open connection per waiting client, so they need an asynchronous I/O model to scale.

Server-Sent Events (SSE)

SSE is a standard for one-way, server-to-client streaming over a single long-lived HTTP response. The client uses the EventSource API. The response has Content-Type: text/event-stream and consists of text events:

Text
id: 42
event: price
data: {"symbol":"ABC","value":101.5}

Events are separated by a blank line.

  • Automatic reconnection: the browser reconnects after a drop, and sends the Last-Event-ID header with the last id received, so the server can resume the stream.
  • Text only (UTF-8), one direction only. The client sends data through separate ordinary requests. The EventSource API cannot set custom request headers, so authentication uses cookies or a token in the URL.
  • Runs over plain HTTP, so it works with existing authentication, proxies, and load balancers. Under HTTP/1.1 browsers limit connections per host (about six), which restricts the number of SSE streams per site. HTTP/2 multiplexing removes this limit.
  • Proxies that buffer responses can break it. Servers disable buffering for the stream.

SSE fits notifications, live feeds, progress updates, and streaming output where the client only listens.

WebSockets

WebSocket provides a full-duplex, persistent, bidirectional channel between client and server over a single TCP connection. Both sides can send messages at any time, with small framing overhead (a few bytes per frame after the connection is established).

Handshake

A WebSocket connection starts as an HTTP/1.1 request that asks to upgrade the protocol:

Text
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

The server replies:

Text
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <base64 of SHA-1 of key + fixed GUID>
sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: HTTP GET with Upgrade: websocket
    S-->>C: 101 Switching Protocols
    Note over C,S: TCP connection now carries WebSocket frames
    C->>S: message frame
    S->>C: message frame
    S->>C: ping
    C->>S: pong
    C->>S: close frame
    S->>C: close frame

Sec-WebSocket-Accept proves the server understood the WebSocket protocol; it is not authentication. After the 101 response the same TCP connection carries frames: text, binary, and control frames (ping, pong, close). Client-to-server frames are masked. Secure connections use wss:// (WebSocket over TLS). Authentication is typically done during the handshake (cookie or token in the request or a first message), because browsers cannot set custom headers on the WebSocket constructor. The server should also validate the Origin header to prevent cross-site hijacking.

WebSocket fits chat, multiplayer games, collaborative editing, and trading UIs, where messages flow both ways with low latency.

Comparison

Aspect Short polling Long polling SSE WebSocket
Direction Client pulls Client pulls (server holds) Server to client Bidirectional
Latency Up to one interval Low Low Lowest
Overhead per message Full HTTP request Full HTTP request Small event framing Small frame
Connection New or reused per request Re-established after each event One long-lived One long-lived
Reconnection and resume Not needed Client logic Built in Client logic
Works via HTTP proxies Yes Yes Mostly Needs proxy support for Upgrade
Data types Any Any Text Text and binary

Heartbeats and connection liveness

A TCP connection can appear open while the peer is gone (a client loses network, a NAT or load balancer times out an idle flow). Idle connections are often closed by intermediaries after a period such as one minute.

  • WebSocket has ping/pong control frames. One side sends ping at an interval, and the other must reply with pong. If no pong arrives within a timeout, the connection is closed and resources are freed.
  • SSE servers send comment lines (starting with :) periodically as keep-alive.
  • Application-level heartbeats can detect a stuck application even when TCP is healthy.
  • Clients reconnect with exponential backoff and jitter (see timeouts, retries, backoff, and jitter) so that a server restart does not cause all clients to reconnect at the same instant. On reconnect, the client sends the last message ID or cursor it processed, and the server replays missed messages.

Scaling persistent connections

Persistent connections change the scaling model from many short requests to many long-lived connections, each holding a socket, buffers, and possibly per-user state.

  • Connection capacity: one server can hold tens or hundreds of thousands of idle connections when using event-driven I/O, limited by memory, file descriptors, and kernel settings. Load is measured in concurrent connections and message rate.
  • Capacity arithmetic: 1,000,000 concurrent users at 100,000 connections per server need at least 10 servers, and more for headroom and failure of a node. At an assumed 10 KB of buffers and state per connection, 100,000 connections use about 1 GB of memory per server. Heartbeats add load too: with a 30 second ping interval, 1,000,000 connections generate about 33,000 pings per second and as many pongs.
  • Stateful connections: a message for user B must reach the server holding B's connection, which may differ from the server of the sender.
  • Solution: a pub/sub backplane. Each connection server subscribes (in Redis pub/sub, Kafka, or another broker) to channels for its connected users or rooms. A sender publishes to the channel and the right server delivers. Alternatively, a connection registry maps user ID to server, and messages are routed directly.
  • Load balancing: the load balancer must support long-lived connections and the Upgrade handshake. Least-connections balancing spreads connection counts. Idle timeouts on the balancer must exceed the heartbeat interval.
  • Deployments and restarts drop connections, so servers drain gradually and clients reconnect with jitter.
  • Fan-out to large rooms is costly: one message becomes many writes. A room of 10,000 members receiving 1 message per second requires 10,000 socket writes per second for that room alone. Batching, and separating the connection tier from business logic, help.
  • Backpressure: a slow client must not consume unbounded buffers. Servers cap per-connection queues and drop or disconnect slow consumers.
flowchart LR
    A["Client A"] <--> W1["WebSocket server 1"]
    B["Client B"] <--> W2["WebSocket server 2"]
    W1 --> P["Pub/sub backplane"]
    W2 --> P
    P --> W1
    P --> W2
    W1 --> API["Application services"]

Choosing

  • Updates are rare and delay is tolerable: short polling.
  • Server push over restricted infrastructure with no need for client-to-server streaming: SSE, or long polling as a fallback.
  • Bidirectional, frequent, low-latency messages: WebSocket.

Interview questions

  • Why is long polling more efficient than short polling? The server answers only when data exists, so there are no empty responses at fixed intervals, and delivery delay is low.
  • How does the WebSocket handshake work? The client sends an HTTP request with Upgrade: websocket and a random key. The server replies 101 Switching Protocols with a value derived from the key, and the TCP connection then carries WebSocket frames.
  • How do you deliver a message to a user connected to a different server? Publish it to a backplane (pub/sub channel or routing by a user-to-server registry) that the server holding the connection consumes.
  • Why are heartbeats needed? Connections can die silently or be dropped by idle timeouts in intermediaries. Periodic ping/pong detects dead peers and keeps the flow active.