Loading...
Loading...
Sockets are the easy part. Reconnects, ordering, duplicates, and offline are the interview.
Module 14 · Real-Time & Offline Systems
WebSockets, SSE, long polling and polling compared, plus reconnection with backoff, heartbeats, message ordering, deduplication, offline queues, and sync on reconnect.
Polling, long polling, SSE, and WebSockets compared, then the reliability layer: reconnection with backoff and jitter, heartbeats, message ordering, deduplication, gap-fill resync, offline outbox queues, and optimistic message states.
Choosing WebSockets is the easy half of a real-time answer. The half that earns the offer is what happens when the socket drops, the tab sleeps, messages arrive twice, or the user goes offline mid-send.
This module compares polling, long polling, SSE, and WebSockets, then works through the client-side reliability layer every chat and live dashboard needs.
You will learn exponential backoff with jitter on reconnect, heartbeat and ping/pong dead-connection detection, monotonic sequencing for ordering, idempotent client message IDs for deduplication, and gap-fill resync via a REST catch-up call.
It finishes with offline behavior: outbox queues, optimistic message states (sending, sent, failed), conflict handling, and how Service Workers and IndexedDB keep the UI usable without a network.
Phase 8 (Week 8) of the interview roadmap. Senior Track spans modules 1–19; the Staff Track starts at module 20.
Picking WebSockets is the easy half of a real-time answer. The half that earns the offer is what happens when the connection drops, the tab is backgrounded, a message arrives twice, or the user sends while offline. Interviewers probe exactly there.
Clarify latency, direction, and volume before naming a transport.
“Use WebSockets” is the easy half of a real-time answer. The half that earns the offer is what happens when the connection drops, the tab sleeps for an hour, a message arrives twice, or the user hits send with no network.
Ask those five questions out loud. Half of real-time prompts do not actually need a socket, and recognizing that is a stronger signal than reaching for the heaviest tool.
Four options, and the one line that justifies each.
| Transport | Direction | Auto-reconnect | Best for |
|---|---|---|---|
| Polling | Client pull | N/A | Low-frequency status checks |
| Long polling | Client pull, held open | Manual | Legacy fallback |
| SSE | Server → client | Built in | Feeds, notifications, token streams |
| WebSocket | Bidirectional | Manual | Chat, presence, collaboration |
Boring, cheap, and frequently correct.
Order status, build progress, and background job results rarely need sub-second latency. Polling every few seconds avoids sticky sessions, connection limits, and a whole class of reconnect bugs.
One-way streaming with reconnection and replay for free.
const es = new EventSource("/api/notifications"); es.addEventListener("message", (e) => { append(JSON.parse(e.data)); }); // The browser reconnects automatically and replays Last-Event-ID. es.onerror = () => setConnectionState("reconnecting");
Full duplex over one TCP connection, with none of the HTTP conveniences.
type Envelope = | { type: "message"; seq: number; clientId: string; body: string } | { type: "presence"; userId: string; online: boolean } | { type: "ping" } | { type: "pong" };
Every long-lived connection dies. Plan the comeback.
let attempt = 0; function connect() { const ws = new WebSocket(url); ws.onopen = () => { attempt = 0; // reset only after a real open resubscribe(); flushOutbox(); }; ws.onclose = () => { // Full jitter, capped at 30s, to avoid a thundering herd. const ceiling = Math.min(30_000, 500 * 2 ** attempt++); setTimeout(connect, Math.random() * ceiling); }; }
The connection that looks open but is already dead.
TCP can take minutes to notice a vanished peer, and proxies, load balancers, and mobile radios routinely drop idle connections without sending a close frame. A ping/pong at the application level detects a zombie socket in seconds.
Order by server sequence, never by client clock.
A single WebSocket preserves order within itself, but a reconnect creates a new stream. Ordering guarantees only hold if the sequence is server-assigned and carried across connections.
At-least-once delivery means you will see the same message twice.
const seen = new Set<string>(); function onMessage(msg: Envelope) { if (msg.type !== "message") return; if (seen.has(msg.clientId)) return; // replay after reconnect seen.add(msg.clientId); appendMessage(msg); }
Reconnecting is not the same as catching up.
async function resync(channelId: string, lastSeq: number) { const res = await fetch(`/api/channels/${channelId}/since?seq=${lastSeq}`); const { messages, truncated } = await res.json(); if (truncated) return hardRefresh(channelId); // gap too large to patch messages.forEach(onMessage); }
Sends made without a network must not silently vanish.
Mobile browsers discard backgrounded tabs aggressively. An in-memory outbox loses the user’s unsent message when the OS reclaims the tab, which is precisely the moment they were offline.
Show the message instantly, but be honest about its status.
| State | Trigger | UI |
|---|---|---|
| Pending | Queued locally | Dimmed with a clock icon |
| Sent | Server ack received | Single tick |
| Delivered | Recipient ack | Double tick |
| Failed | Retries exhausted | Red marker with retry action |
Slack or WhatsApp, in the shape an interviewer expects.
WebSocket (auth at handshake) ↕ Connection manager backoff · heartbeat · resubscribe ↓ Message pipeline dedupe by clientId → order by seq → gap-fill via REST ↓ Message store (IndexedDB + in-memory) ↓ Virtualized message list ← outbox (pending sends)
“What if the user has 50 tabs open?” Share one connection across tabs using a SharedWorker or elect a leader tab via BroadcastChannel, then fan messages out locally.
The reliability details that separate strong answers from transport trivia.
Real-time and offline systems interview questions.