Search…

Real-time on the frontend

In this series (12 parts)
  1. What frontend system design covers
  2. Rendering strategies: CSR, SSR, SSG, ISR
  3. Performance fundamentals: Core Web Vitals
  4. Loading performance and resource optimization
  5. State management at scale
  6. Component architecture and design systems
  7. Client-side caching and offline support
  8. Real-time on the frontend
  9. Frontend security
  10. Scalability for frontend systems
  11. Accessibility as a system design concern
  12. Monitoring and observability for frontends

Prerequisite: Client-side caching and offline support.

Most frontends start with request-response: the client asks, the server answers. That model breaks down when the data changes faster than the user can refresh. Stock tickers, chat applications, collaborative editors, live dashboards; all of these need data pushed to the client without waiting for a poll.

This article walks through the four main strategies for real-time communication on the frontend, when each one makes sense, and how to handle the messy parts like reconnection and ordering. We will also touch on the networking concepts that underpin each approach and how they relate to real-time systems at the backend level.


Short polling

The simplest approach. The client sends a request on a fixed interval and processes whatever comes back.

sequenceDiagram
  participant Client
  participant Server

  loop Every N seconds
      Client->>Server: GET /api/updates
      Server-->>Client: Current state (possibly unchanged)
  end

Short polling sends requests at a fixed interval regardless of whether new data exists.

When to use it

  • Low update frequency (minutes, not seconds).
  • Backend cannot support persistent connections.
  • Simplicity matters more than latency.

Trade-offs

  • Wastes bandwidth when nothing has changed.
  • Update latency equals the poll interval.
  • Easy to implement and debug. No special infrastructure needed.

Set your interval based on the acceptable staleness. A dashboard refreshing every 30 seconds is fine. A chat app polling every 5 seconds feels sluggish.


Long polling

Long polling improves on short polling by keeping the connection open until the server has something to send.

sequenceDiagram
  participant Client
  participant Server

  Client->>Server: GET /api/updates (held open)
  Note over Server: Waits for new data...
  Server-->>Client: New data arrives, respond
  Client->>Server: GET /api/updates (new request)
  Note over Server: Waits again...
  Server-->>Client: New data arrives, respond

Long polling holds the request open until the server has data to send, then the client immediately reconnects.

When to use it

  • You need lower latency than short polling but WebSocket infrastructure is not available.
  • Moderate update frequency.
  • You want to work through HTTP proxies and load balancers without configuration changes.

Trade-offs

  • Each response triggers a new request, so there is a brief gap.
  • Holding thousands of open connections requires async server frameworks (not thread-per-request).
  • Timeout handling is tricky. Set a server-side timeout (30 to 60 seconds), return an empty response, and let the client reconnect.

Server-Sent Events (SSE)

SSE uses a single, long-lived HTTP connection over which the server pushes text events. The browser handles reconnection automatically through the EventSource API.

sequenceDiagram
  participant Client
  participant Server

  Client->>Server: GET /api/stream (Accept: text/event-stream)
  Server-->>Client: event: update (data: ...)
  Server-->>Client: event: update (data: ...)
  Server-->>Client: event: update (data: ...)
  Note over Client: Connection stays open
  Server--xClient: Connection drops
  Note over Client: Auto-reconnects after retry interval
  Client->>Server: GET /api/stream (Last-Event-ID: 42)
  Server-->>Client: event: update (data: ...)

SSE maintains a unidirectional stream from server to client with built-in reconnection and event ID tracking.

When to use it

  • Server-to-client only (notifications, live feeds, stock prices).
  • You want automatic reconnection without writing it yourself.
  • HTTP/2 multiplexing is available, eliminating the 6-connection browser limit.

The API

const source = new EventSource('/api/stream');

source.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  updateUI(data);
});

source.addEventListener('error', () => {
  // Browser handles reconnection automatically
  console.log('Connection lost, reconnecting...');
});

SSE supports a Last-Event-ID header on reconnection. If the server tracks event IDs, the client catches up on missed events without polling for the gap.


WebSockets

WebSockets provide full-duplex communication over a single TCP connection. Both client and server can send messages at any time.

sequenceDiagram
  participant Client
  participant Server

  Client->>Server: HTTP Upgrade request
  Server-->>Client: 101 Switching Protocols
  Note over Client,Server: Full-duplex WebSocket connection
  Client->>Server: message (user typing)
  Server-->>Client: message (new chat message)
  Client->>Server: message (cursor position)
  Server-->>Client: message (presence update)

WebSocket upgrades an HTTP connection to a persistent, full-duplex channel.

When to use it

  • Bidirectional communication (chat, collaboration, gaming).
  • Low latency in both directions.
  • High message frequency.

Trade-offs

  • Requires WebSocket-aware load balancers and proxies.
  • No built-in reconnection. You must implement it yourself.
  • Binary and text frames supported. Use binary for high-throughput scenarios.
const ws = new WebSocket('wss://api.example.com/ws');

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'room-42' }));
});

ws.addEventListener('message', (event) => {
  const msg = JSON.parse(event.data);
  handleMessage(msg);
});

ws.addEventListener('close', () => {
  // Reconnect with exponential backoff
  scheduleReconnect();
});

Choosing the right strategy

Lower latency comes with higher implementation and infrastructure complexity.

FactorShort PollingLong PollingSSEWebSocket
DirectionClient to serverClient to serverServer to clientBidirectional
LatencyHighMediumLowLowest
ReconnectionN/AManualAutomaticManual
HTTP/2 friendlyYesYesYesNo (upgrades)
Proxy friendlyYesMostlyYesNeeds config

Handling reconnection

For WebSockets, implement exponential backoff with jitter:

function scheduleReconnect(attempt = 0) {
  const baseDelay = 1000;
  const maxDelay = 30000;
  const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
  const jitter = delay * (0.5 + Math.random() * 0.5);

  setTimeout(() => {
    const ws = new WebSocket(url);
    ws.addEventListener('open', () => { attempt = 0; });
    ws.addEventListener('close', () => scheduleReconnect(attempt + 1));
  }, jitter);
}

Jitter prevents a thundering herd. If your server restarts and 10,000 clients all reconnect at the same instant, the server goes down again. Jitter spreads them out.

For SSE, the browser reconnects automatically but you can control the delay by having the server send a retry: field in the event stream.


Live collaboration concepts

Collaborative editors like Google Docs need more than a WebSocket. They need a conflict resolution strategy.

Operational Transformation (OT) models every edit as an operation (insert character at position 5, delete range 10 to 15). When two users edit simultaneously, the server transforms one operation against the other so both converge to the same document state.

CRDTs (Conflict-free Replicated Data Types) encode the data structure itself in a way that concurrent edits always merge deterministically. Libraries like Yjs and Automerge implement CRDTs for text, arrays, and maps.

The trade-off: OT requires a central server to order operations. CRDTs work peer-to-peer but use more memory to track causality metadata.

For most applications, using a library (Yjs, Automerge, or a hosted service like Liveblocks) is the right call. Implementing OT or CRDTs from scratch is a multi-month project.


Presence indicators

Showing who is online and where their cursor is. This is a separate concern from document sync.

Presence data is ephemeral. It does not need to be persisted. A simple pattern:

  1. Each client sends a heartbeat every 10 to 15 seconds over the WebSocket.
  2. The server tracks the last heartbeat timestamp per user per room.
  3. If a heartbeat is missed for 30 seconds, the user is marked as away.
  4. Presence updates are broadcast to other users in the room.

Keep presence payloads small. Send only what changed: cursor position, selection range, user ID. Do not send the entire user profile on every heartbeat.


What comes next

Real-time data flows open up new attack surfaces. Frontend security covers XSS, CSRF, CORS, and how to protect your application when data is flowing in both directions.

Start typing to search across all content
navigate Enter open Esc close