Frontend System Design/Real-time Updates
Lesson 19 of 19 · Episode 19

Real-time Updates — WebSockets

Full-duplex text and binary frames, protocol upgrades, recovery, multiplexing, authentication, and production architecture trade-offs.

WebSocketsFull duplexReconnectionMultiplexing
Watch on YouTube ↗

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.

The missing direction

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.

Both peers can speak whenever they needSOCKET OPEN
▱
Clientsend + receive
▶▶
cursor.move · x:218 y:96
TEXT FRAME
▤
Serversend + receive
CLIENT →
text
← SERVER
text
CLIENT →
binary
← SERVER
text

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 EventsWebSockets
DirectionServer → clientClient ↔ server
ProtocolLong-lived HTTP responseHTTP handshake, then WebSocket
PayloadUTF-8 text eventsText or binary frames
ReconnectNative EventSource behaviorApplication-owned
Best shapeOne command, many server updatesFrequent updates from both sides
Protocol switch

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.

HTTP handshake → WebSocket channelNEGOTIATING
▱
BrowserCONNECTING
GET /socket · Upgrade: websocket
HTTP request
Browser asks to upgrade
https://request + headers
▤
ServerUPGRADE?
REQUEST
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: …
RESPONSE
HTTP/1.1 101
Connection: Upgrade
Upgrade: websocket
The opening handshake
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=
101 is not a normal success body
It confirms a protocol switch. After the response headers, the same underlying connection stays open, but HTTP messages give way to WebSocket frames. The optional subprotocol lets both peers agree on a higher-level contract such as chat.v2 or GraphQL transport.
Browser and wire

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.

Native browser WebSocket
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.

Watch bufferedAmount
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.
Resilience

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.

Recovery is an application protocolLIVE
Live
Drop
Buffer
Back off
Resume
Replay
Flush
Live again
▱
Clientcursor: 41
event · seq 41
Live
seq 41 received
outbox: 0attempt: 0
▤
Serverhistory: 41–43
Detect
close + timeout
Retry
backoff + jitter
Resume
sequence cursor
Buffer
unsent commands
A reconnect delay with jitter
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.

Do not create a reconnect stampede
If ten thousand clients retry at exactly 1, 2, 4, and 8 seconds, a recovering server receives synchronized traffic spikes. Jitter spreads those retries across time so recovery does not immediately cause another outage.
Connection ownership

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.

Idle is not the same as deadHEALTHY
▱
BrowserALIVE
ping · 12:04:08
heartbeat deadline resets after every pong
▤
ServerALIVE

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.

Many logical channels · one physical socketMULTIPLEXED
▱
Client1 connection
channel:work · typing · sara
Work envelope
shared heartbeat · shared reconnect · shared TLS
▥
Gatewayroutes by ID
A multiplexed message envelope
{
  "type": "message.created",
  "channelId": "family",
  "sequence": 8421,
  "payload": {
    "id": "msg-91",
    "text": "Dinner at 8"
  }
}
Multiplexing trades isolation for efficiency
One socket centralizes lifecycle cost and avoids per-origin connection pressure. It also becomes a shared failure domain: a stalled connection affects every logical channel. Prioritize traffic, cap queues, and keep a resume cursor per channel when their histories advance independently.
Architecture

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.

Choose where commands and state travelEVERYTHING ON WS
▱
Client
WS · send command
STEP 1 / 3
▤
Server
Command and event share the socket

Lowest coordination latency. The connection protocol owns validation, acknowledgements, retries, and delivery semantics.

Good fit: chat, cursors, live games
PatternTrade-off
Everything over WebSocketLowest coordination latencyYou own the complete command protocol and delivery semantics
HTTP command + WS resultHTTP validates and queues work cleanlyTwo transports must correlate job and update IDs
Notify + fetchDurable, cacheable server snapshotsAdds 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.

Security

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.

Key idea
WebSockets solve bidirectional transport. They do not solve your application protocol, persistence, ordering, authorization, observability, or horizontal scaling. Choose how much of that stack your team truly wants to own.
The real-time spectrum

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.

Let requirements choose the transportDECISION LAB
START WITH
WebSockets

Continuous full-duplex traffic makes one low-overhead channel worth its lifecycle cost.

A practical starting map

Choose per interaction, not per product
A chat product may use HTTP for history, WebSockets for messages and presence, and polling for a low-priority admin dashboard. “The app uses WebSockets” is less useful than documenting the requirement and delivery contract for each flow.
Practice

Check your understanding

Q1Multiple choice
A server accepts a browser's WebSocket upgrade request. What does HTTP status 101 mean here?
Q2Multiple choice
A WebSocket reconnects after the client last processed sequence 41. What is required to receive missed updates safely?
Q3Multiple choice
Why add an application heartbeat when an idle socket may carry no business events for minutes?
Q4Multiple choice
Which architecture best preserves a cacheable, durable HTTP source of truth while still notifying clients immediately?
Q5Multiple choice
Why is a reusable access token in a WebSocket query string risky?
Q6Sort each scenario
Choose the better starting transport for each requirement.
Collaborative cursors moving many times per second
A dashboard that may be five minutes stale
Low-latency bidirectional binary audio chunks
One export command followed by server-only progress text
Try it yourself

Design challenge

Design the realtime layer for a collaborative whiteboard with cursors, shapes, comments, presence, and offline recovery. Write down:

  1. The message envelope: type, channel, request ID, sequence, and payload.
  2. Which updates are durable, which are disposable, and which may be coalesced under backpressure.
  3. The heartbeat deadline, reconnect backoff, jitter, resume cursor, and offline outbox policy.
  4. Whether commands travel over WebSocket, HTTP, or a mixture — and why.
  5. How the handshake authenticates, how subscriptions are authorized, and how credentials rotate.
  6. How gateways, workers, persistence, and fan-out scale across multiple backend instances.
Key takeaways
  • →A WebSocket begins as an HTTP upgrade and becomes one persistent full-duplex text-and-binary channel after a 101 response.
  • →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.
← Previous
18. Real-time Updates — Server-Sent Events