跪拜 Guibai
← All articles
Frontend

Hardening WebSocket for Real-Time Dashboards: Heartbeats, Resumable Streams, and Idempotent Consumption

By one_last_FE ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Dashboard teams often treat WebSocket as a solved problem because Socket.IO handles transport-level reconnect, but transport liveness says nothing about the business data link. Without sequence-based catch-up and client-side deduplication, a brief network blip causes permanent data gaps or duplicate events that corrupt map state and counters.

Summary

Real-time map dashboards break badly when a WebSocket drops: drone positions jump, events roll back, and full-page refreshes cause visible flicker. A naive reconnect-and-refetch loop makes this worse by hammering the server and discarding in-flight messages. The fix layers four mechanisms on top of Socket.IO: a business heartbeat that detects stalled data links, exponential backoff with random jitter to spread reconnection storms, a server-assigned monotonic sequence number so the client can request only missed messages on reconnect, and an LRU-based message ID set that makes repeated deliveries idempotent. Incoming messages are converted into upsert/delete patches and fed directly to the map’s incremental update pipeline, so the display never clears and redraws. The approach requires the backend to store a short rolling window of sequenced messages and to accept a `lastSeq` parameter during client login, but it eliminates the flicker, data gaps, and double-processing that plague dashboard WebSocket implementations.

Takeaways
Socket.IO’s built-in ping/pong only confirms transport liveness; a separate business heartbeat carrying `clientId`, `lastSeq`, and a timestamp detects stalled backend data pipelines.
Exponential backoff with random jitter (base 1 s, cap 30 s, plus up to 1 s of jitter) prevents every dashboard client from reconnecting in the same instant after a network outage.
Every message needs a server-assigned, globally incrementing `seq` so the client can detect gaps and request exactly the missed range on reconnect.
A unique `messageId` per message, combined with a client-side LRU Set (e.g., last 5,000 IDs), makes repeated deliveries from the server’s catch-up window idempotent.
Reconnection must not trigger a full data refetch; the client sends `lastSeq` during login, the server replays only messages after that sequence, and the frontend converts them into incremental map patches.
Persisting `lastSeq` to `localStorage` lets the client resume correctly even after a page reload.
Converting WebSocket payloads into `{ upserts: [], removeIds: [] }` patches and feeding them to an existing `applyMarkerPatch` method avoids any map clear-and-redraw flicker.
An `off` method that removes specific handlers prevents stale listeners from firing after a Vue component is destroyed.
Conclusions

Most WebSocket instability in dashboards isn’t a connection problem; it’s a state-recovery problem. The connection always comes back, but the map state drifts because the client has no way to ask ‘what did I miss?’

Socket.IO’s reconnection is a transport primitive, not a data-integrity primitive. Treating it as the latter is why so many dashboard teams accept periodic flicker and data gaps as inevitable.

Business heartbeats double as a cheap consumption checkpoint: piggybacking `lastSeq` on the ping lets the server track progress without a separate ACK storm.

An LRU dedup window of a few thousand IDs is sufficient because the server’s replay window is also time- or count-bounded; the two windows only need to overlap slightly.

Disabling Socket.IO’s own reconnection and replacing it with a single, tunable backoff loop gives the frontend team full control over retry timing and avoids two competing reconnect mechanisms.

Concepts & terms
Business heartbeat
An application-level ping/pong distinct from transport-level keep-alives. The client sends a ping carrying `clientId`, `lastSeq`, and a timestamp; if no pong arrives within a timeout (e.g., 30 s), the client assumes the backend data pipeline is stalled and forces a reconnect.
Exponential backoff with jitter
A reconnection strategy where the delay doubles after each failure (e.g., 1 s, 2 s, 4 s, up to a cap of 30 s) plus a random number of milliseconds (e.g., 0–1000 ms) to spread out reconnection attempts across many clients.
Resumable transfer (breakpoint resume)
The client remembers the last successfully consumed sequence number (`lastSeq`) and sends it during reconnection login. The server replays only messages with `seq > lastSeq` from a short-lived cache, avoiding both data loss and a full data refetch.
Message deduplication
Using a unique `messageId` per message and a client-side LRU Set to detect and discard messages that have already been processed, making repeated deliveries from the server’s catch-up window safe.
Incremental patch
Instead of replacing the entire dataset, each WebSocket message is converted into a small `{ upserts: [...], removeIds: [...] }` structure that the map renderer merges into its existing state, updating only the changed entities.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗