Real-time Updates — WebSockets
Full-duplex text and binary frames, protocol upgrades, recovery, multiplexing, authentication, and production architecture trade-offs.
Polling lets the client ask. Server-Sent Events lets the server keep answering. WebSockets remove that asymmetry: after one handshake, the browser and server share a persistent, full-duplex channel where either side can send text or binary frames at any time.
Both peers need to speak without waiting
Short and long polling are client-driven. SSE is server-driven after the client opens the response. Those shapes work beautifully until both peers produce frequent updates: chat messages, collaborative cursors, multiplayer input, presence, or live audio. Opening another HTTP request for every client update adds ceremony exactly where the product wants a fast conversation.
A WebSocket is a single full-duplex connection. Client and server can write independently, and frames may contain UTF-8 text or binary bytes. That flexibility is the advantage — and the reason the application must define its own message schema, acknowledgements, ordering, authorization, and recovery rules.
| Server-Sent Events | WebSockets | |
|---|---|---|
| Direction | Server → client | Client ↔ server |
| Protocol | Long-lived HTTP response | HTTP handshake, then WebSocket |
| Payload | UTF-8 text events | Text or binary frames |
| Reconnect | Native EventSource behavior | Application-owned |
| Best shape | One command, many server updates | Frequent updates from both sides |
It begins as HTTP, then becomes something else
The browser starts with an ordinary HTTP GET and asks the server to upgrade the connection. The server may reject that request before any socket exists. If it accepts, it replies with status 101 Switching Protocols. From that point forward, the bytes on the TCP connection follow WebSocket framing instead of HTTP request/response semantics.
GET /socket HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat.v2
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=chat.v2 or GraphQL transport.A tiny API over small frames
The native browser API is intentionally small: construct the socket, observe four lifecycle events, call send(), and eventually call close(). Everything above that — message types, validation, retries, queues, and resume cursors — belongs to your application or a library.
const socket = new WebSocket("wss://example.com/socket", ["chat.v2"]);
socket.addEventListener("open", () => {
socket.send(JSON.stringify({
type: "chat.send",
requestId: crypto.randomUUID(),
payload: { channelId: "family", text: "Hello" },
}));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
routeMessage(message);
});
socket.addEventListener("error", reportTransportError);
socket.addEventListener("close", scheduleReconnect);
// Deliberate shutdown:
socket.close(1000, "page finished");Once connected, a WebSocket frame adds only a small header instead of a fresh set of HTTP headers for every update. That is why frequent tiny messages can be dramatically cheaper than polling. The connection itself is not free, though: it consumes server memory, file descriptors, load-balancer state, heartbeats, and operational attention for as long as it remains open.
socket.send() queues bytes; it does not promise that the peer has received or processed them. A fast producer can outrun the network. Monitor bufferedAmount, coalesce disposable updates such as cursor positions, and apply backpressure before memory grows without bound.Reconnection is only the first half of recovery
Native WebSockets do not reconnect, remember event IDs, or replay history. A production client must detect a dead connection, retry with exponential backoff and jitter, tell the server where it stopped, and decide what to do with commands created while offline.
function reconnectDelay(attempt: number) {
const cap = _000;
const exponential = Math.min(cap, _000 * 2 ** attempt);
return Math.random() * exponential; // full jitter
}
function resume(socket: WebSocket, lastSequence: number) {
socket.send(JSON.stringify({
type: "resume",
afterSequence: lastSequence,
}));
}A resume cursor is an application contract, not a WebSocket feature. The server needs retained history or a fresh snapshot; the client needs idempotency keys for retried commands. Without those pieces, reconnecting merely creates a new empty pipe after some updates may already be lost.
Heartbeats detect silence; multiplexing contains cost
Silence needs a deadline
No messages may mean “nothing changed,” or it may mean the network died without a clean close event. Protocol-level ping/pong exists, but browser JavaScript cannot directly control those frames. Many applications add their own ping and pong messages and close a socket when a deadline passes.
One connection can carry many logical channels
Opening a separate socket for every chat room or product feature repeats the handshake, TLS state, heartbeat, and recovery machinery. Most large applications multiplex logical channels over one physical socket by placing a channel or topic identifier in every message envelope.
{
"type": "message.created",
"channelId": "family",
"sequence": 8421,
"payload": {
"id": "msg-91",
"text": "Dinner at 8"
}
}The socket does not have to carry everything
A transport choice and a message-flow choice are separate decisions. Some products send commands and events over the same WebSocket. Others keep client commands on HTTP and reserve the socket for updates. A third pattern sends only invalidation notices and fetches durable state over HTTP.
| Pattern | Trade-off | |
|---|---|---|
| Everything over WebSocket | Lowest coordination latency | You own the complete command protocol and delivery semantics |
| HTTP command + WS result | HTTP validates and queues work cleanly | Two transports must correlate job and update IDs |
| Notify + fetch | Durable, cacheable server snapshots | Adds a follow-up read and slightly more latency |
The hybrid patterns are especially useful for heavy asynchronous work. A POST can validate a video-generation command and return 202 Accepted; workers continue independently; the WebSocket later publishes progress. The socket never has to pretend a long-running job is one synchronous request.
Authenticate the handshake or the first message deliberately
The browser constructor accepts a URL and optional subprotocols, but not arbitrary authorization headers. That forces an architectural choice: validate credentials before the upgrade, or open the socket in a tightly restricted state and authenticate immediately afterward.
Native API, library, or managed service?
Raw WebSockets give maximum control and minimum abstraction, but you own reconnection, acknowledgements, rooms, presence, schema evolution, and operations. A library such as Socket.IO adds a higher-level protocol and fallbacks. A managed realtime provider can also absorb connection gateways and fan-out. Each step removes infrastructure work while adding a dependency, pricing model, and its own semantics.
Requirements choose the connection
Ask five questions before choosing a transport: How fresh must the UI be? Which direction does data travel? How frequent are updates? Can the environment sustain the connection? Can the team operate recovery and scaling? Change the answers below and watch the starting recommendation change.
A practical starting map
- Short polling: simple infrastructure, rare updates, and acceptable staleness.
- Long polling: broader compatibility with lower latency, but repeated request lifecycles.
- Server-Sent Events: frequent server-driven text updates with simpler browser recovery.
- WebSockets: frequent, latency-sensitive, bidirectional or binary communication.
Check your understanding
Design challenge
Design the realtime layer for a collaborative whiteboard with cursors, shapes, comments, presence, and offline recovery. Write down:
- The message envelope: type, channel, request ID, sequence, and payload.
- Which updates are durable, which are disposable, and which may be coalesced under backpressure.
- The heartbeat deadline, reconnect backoff, jitter, resume cursor, and offline outbox policy.
- Whether commands travel over WebSocket, HTTP, or a mixture — and why.
- How the handshake authenticates, how subscriptions are authorized, and how credentials rotate.
- How gateways, workers, persistence, and fan-out scale across multiple backend instances.
- →A WebSocket begins as an HTTP upgrade and becomes one persistent full-duplex text-and-binary channel after a
101response. - →Small frames make frequent updates efficient, but every open connection still consumes infrastructure and operational capacity.
- →Native WebSockets provide transport, not recovery: detection, backoff, jitter, resume cursors, replay, deduplication, and offline buffering are application responsibilities.
- →Heartbeats distinguish idle from dead; multiplexing lets many logical topics share one physical connection.
- →Commands and state can use different paths: all-WebSocket, HTTP command plus WebSocket update, or WebSocket notification plus HTTP fetch.
- →Authentication is a handshake design decision, while authorization must still be checked for every channel and operation.
- →Freshness, direction, frequency, environment, and operational cost — not fashion — choose the realtime transport.