WebSockets & Real-Time
WebSocket handshake and framing, Socket.IO, Server-Sent Events, connection lifecycle, keepalive mechanisms, backpressure handling, and the differences between each real-time protocol.
WebSocket handshake and framing, Socket.IO, Server-Sent Events, connection lifecycle, keepalive mechanisms, backpressure handling, and the differences between each real-time protocol.
Real-time communication on the web has evolved from polling-based workarounds to dedicated protocols designed for low-latency, bidirectional data flow. WebSockets provide a full-duplex channel over a single TCP connection, Server-Sent Events (SSE) offer efficient server-to-client streaming, and long polling remains the fallback for legacy environments. Understanding when and how to use each is essential for building responsive applications.
The WebSocket protocol (RFC 6455) begins with an HTTP Upgrade handshake. The client sends a standard HTTP GET request with the headers Upgrade: websocket and Connection: Upgrade, along with a Sec-WebSocket-Key — a base64-encoded 16-byte random value. The server responds with 101 Switching Protocols and a Sec-WebSocket-Accept header, which is the SHA-1 hash of the key concatenated with a fixed GUID. Once the handshake completes, the TCP connection upgrades to the WebSocket protocol and both sides can send data freely.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=After the upgrade, data is transmitted in frames. Each frame has an opcode (1 for text, 2 for binary, 8 for close, 9 for ping, 10 for pong), a payload length, and a mask flag. Client-to-server frames must be masked (XOR-obfuscated with a 32-bit masking key) to prevent cache poisoning attacks on intermediary proxies. Server-to-client frames are unmasked. The frame format is compact — as little as 2 bytes overhead for small messages — making it far more efficient than HTTP polling.
WebSocket Frame (client to server):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|R|R|R| opcode |M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Masking key (32 bits, if MASK=1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload Data (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+The connection lifecycle follows a simple state machine: opening handshake, then bidirectional data transfer, and finally a close handshake (a close frame from either side, acknowledged by the other). Ping/pong frames serve as keepalive probes — either side may send a ping, and the peer must reply with a pong. This is critical for detecting half-open connections (e.g., a client that disconnected without sending a close frame).
Server-Sent Events (SSE, part of HTML5) provide a simpler alternative when only server-to-client streaming is needed. The client opens an EventSource connection to a URL, and the server responds with Content-Type: text/event-stream. The connection remains open and the server pushes events as data: lines. SSE supports automatic reconnection — if the connection drops, the browser retries automatically using the Last-Event-ID header. This makes SSE ideal for stock tickers, log streaming, status updates, and notification feeds.
GET /events HTTP/1.1
Host: example.com
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"ticker": "AAPL", "price": 189.45}
data: {"ticker": "GOOG", "price": 141.23}
event: status
data: {"connected": true}Long polling is the oldest real-time workaround. The client sends a request, and the server holds the response open until new data is available or a timeout expires. When the client receives the response, it immediately sends a new request. This creates many short-lived HTTP connections and introduces significant latency compared to WebSockets or SSE. It also adds overhead from HTTP headers on every poll cycle. Long polling should be considered a fallback for environments where WebSockets or SSE are unavailable (e.g., corporate proxies that strip the Upgrade header).
Socket.IO is the most widely used abstraction layer for real-time in the Node.js ecosystem, powering approximately 80% of Node.js real-time applications. It provides WebSocket as the primary transport with automatic fallback to HTTP long polling when WebSockets are blocked. Socket.IO adds higher-level concepts: rooms (logical channels within a server), namespaces (separate multiplexed channels on a single connection), and automatic reconnection with exponential backoff. The library handles edge cases like session recovery and packet buffering during reconnection.
The simulator below lets you compare WebSocket, SSE, and long polling side by side. See how each protocol handles message delivery, observe the connection overhead, and toggle network conditions to understand how they degrade differently.
WebSockets are the foundation of modern real-time applications: chat platforms (Slack, Discord), live notifications (GitHub, Jira), collaborative editing (Google Docs, Figma), financial trading platforms, multiplayer gaming, and live dashboards. The ability to push messages instantly to hundreds or thousands of connected clients with minimal overhead makes WebSockets the default choice for bidirectional communication.
SSE is often overlooked but is the better choice for many server-to-client streaming use cases. Stock tickers, cryptocurrency price feeds, server log streaming, deployment status updates, and notification systems all benefit from SSE's simpler model. The built-in automatic reconnection and event ID tracking eliminate a significant amount of client-side boilerplate. SSE also works over standard HTTP/2 connections without special proxy configuration.
Long polling should be reserved for legacy environments or as a fallback. The overhead of repeated HTTP request/response cycles — even with keepalive — adds 200-500ms of latency per message compared to WebSockets. In high-throughput scenarios, the connection churn can strain both client and server resources.
Backpressure occurs when a producer sends data faster than the consumer can process it. In WebSocket implementations, the socket.bufferedAmount property reveals how many bytes are queued but not yet sent. When this grows large, the application should slow down or pause production. Ignoring backpressure leads to memory exhaustion and connection timeouts.
Connection health monitoring is essential. Implement ping/pong intervals (every 30-60 seconds is typical) to detect silent disconnections. Set a maxIdleTimeout on the server to clean up zombie connections. Use connection-level rate limiting to prevent a single client from overwhelming the server with messages. In Socket.IO, the maxHttpBufferSize option limits the size of incoming messages, and cors settings control which origins can establish a connection.
bufferedAmount, implement ping/pong intervals, and use connection-level rate limiting to maintain production reliability.1.What happens during the WebSocket handshake?
2.When would you choose Server-Sent Events over WebSockets?
3.What is backpressure in the context of WebSockets?