Prompt and context
This question tests whether a frontend engineer treats WebSocket as an unreliable connection. Wi-Fi changes, sleep, proxy resets, server restarts, and silent half-open sockets are normal. Calling connect() immediately from onclose creates a reconnect storm and cannot recover events missed while offline. Cover client state, backoff, heartbeat, replay, authentication, and page lifecycle.
What the interviewer tests
Strong answers define message semantics and recovery boundaries, then enforce transitions with an explicit state machine. They use jittered exponential backoff, an application heartbeat for half-open detection, sequence or cursor replay for missed events, and queues that distinguish droppable notifications from acknowledged commands. They also discuss background tabs, network changes, expired tokens, server throttling, and user-visible state.
Questions to clarify
- Is a message a droppable notification, a replayable event, or a command that must be processed? Does the server support cursor replay?
- How is authentication refreshed? What happens when a token, permission, or protocol version changes during reconnect?
- Who sends and confirms heartbeats? How long can a proxy silently drop packets?
- Must the client support multiple tabs, mobile backgrounding, browser sleep, and offline transitions?
- Can users keep editing while disconnected? How are conflicts, ordering, and duplicate submissions handled?
30-second answer framework
“I would model the client with idle, connecting, open, suspect, backoff, and closed states. After opening, an application ping/pong with a deadline detects silent failures. Abnormal closure or heartbeat timeout enters jittered exponential backoff so clients do not reconnect together. Every event has a monotonic sequence; after reconnect, the client resumes from its last contiguous cursor before consuming live events. Commands carry idempotency keys and require acknowledgements, while notifications may be dropped. Offline, hidden, and expired-token states pause or re-authenticate with clear user feedback.”
Step-by-step deep answer
Step 1: Define the connection state machine
Centralize state and legal transitions instead of letting callbacks mutate isConnected. A typical path is connecting -> open -> suspect -> backoff -> connecting; a user close enters closed and disables automatic reconnect. Record the reason, attempt count, and connection id for every transition.
Step 2: Distinguish close, error, and half-open sockets
Browser error events may not contain an actionable reason, and close may never arrive for a half-open path. The heartbeat records send time, pong deadline, and last received message. On timeout, close the old socket explicitly before scheduling a new one so two connections cannot deliver duplicate events.
Step 3: Implement backoff and jitter
RFC 6455 warns that persistent immediate reconnects can become a denial-of-service-like storm. Use min(cap, base * 2^attempt) + random(0, jitter) and reset the attempt count only after a stable connection. Respect server maintenance or throttling hints such as Retry-After.
Step 4: Recover an event cursor
Events carry a monotonic stream sequence. The client persists the last contiguous sequence and sends it in the resume handshake. The server returns the missing range, then switches to live delivery. If the cursor expired, return a snapshot version; the client loads it and resumes after the snapshot sequence.
Step 5: Design the send queue and idempotency
Presence and typing notifications may be dropped; edits and payment intents require acknowledgement. Commands carry a clientMessageId, and the server deduplicates by that key and stores the result. Keep a bounded offline queue; when full, stop accepting more commands and explain the state instead of hiding an unbounded memory queue.
Step 6: Handle authentication and protocol changes
Check whether a token is near expiry before reconnecting and refresh it when needed. An unauthorized close code stops blind retries and enters login flow. Include a protocol version in the handshake; negotiate a compatible version or show an upgrade path rather than looping forever.
Step 7: Integrate page and network lifecycle
visibilitychange, online/offline, and mobile backgrounding alter the strategy. A hidden page can reduce heartbeat frequency or pause live delivery; on return, run a health check and cursor sync. Offline stops dialing immediately and online starts a backoff schedule instead of spinning failed attempts.
Step 8: Observe user experience and server pressure
Record connection time, reconnect attempts, heartbeat timeouts, replay count, cursor gaps, and dropped queue items. The server observes concurrent sockets, handshake failures, reconnect peaks, and duplicate messages by tenant and client version. Never log tokens or message bodies. Show “reconnecting” and “caught up” rather than transport error codes.
Minimal state-machine pseudocode
on_open(socket):
state = OPEN
send({type: "resume", lastSeen: cursor})
on_heartbeat_timeout():
socket.close()
state = BACKOFF
delay = min(MAX, BASE * 2 ** attempts) + random(0, JITTER)
schedule(connect, delay)Trade-offs and boundaries
| Decision | Choice | Why |
|---|---|---|
| Reconnect delay | Truncated exponential backoff plus jitter | Reduces synchronized peaks |
| Recovery | Cursor replay, snapshot when needed | Avoids requiring a page refresh |
| Reliability | Droppable notifications, idempotent acknowledged commands | Matches cost to business value |
| Background tabs | Throttle or pause, then sync on return | Saves power and idle connections |
WebSocket provides ordered messages, not business-level exactly-once, offline queues, or state synchronization. Those semantics belong in the application protocol. If a product only needs lossy server push, compare SSE, but do not confuse a transport choice with recovery guarantees.
Rollout plan and evidence
Ship the state machine, heartbeat, and jittered backoff first, then add cursor recovery and idempotent commands. Drill network changes, server restarts, browser sleep, token expiry, and a synchronized disconnect of many clients. RFC 6455 recommends a random initial delay and increasing backoff after abnormal closure; MDN documents browser WebSocket events and readyState.
Pilot exit criteria
The page recovers without refresh; acknowledged commands are neither duplicated nor lost; reconnect peaks do not overload the server; hidden pages do not hold unnecessary sockets; and users can see connection state and last sync time. Any failure means fixing the protocol or lifecycle policy first.
How to prove the gain is real
Compare recovery success, p95 recovery time, duplicate-event rate, cursor gaps, handshake peaks, and mobile energy before and after. Segment by network, browser, and page visibility so an average does not hide one platform’s failures.
Common mistakes and follow-ups
Reconnecting immediately in onclose
When a server restarts, every client dials at once. Use backoff, jitter, and server throttling hints, and reset attempts only after stability.
Relying only on close
A half-open path may never emit close. Use a heartbeat and deadline, then close the stale socket yourself.
Assuming a successful reconnect means complete data
Connection recovery does not replay events missed offline. Use a last-seen cursor, snapshot version, and contiguous sequence checks.
How do you prevent duplicate commands?
Give every command a client idempotency key, persist the server result, and remove the queue item only after acknowledgement or a result query.
What if the token expires during reconnect?
Refresh and handshake again. An unauthorized close stops retries and enters login flow; it is not a transient network error.
How do you control connections across tabs?
Use BroadcastChannel or SharedWorker so one tab owns the socket and others subscribe. Transfer ownership when the owner disappears and revalidate the cursor.