Short answer: treat a browser reconnect as a fresh snapshot plus an ordered event stream, and show unknown until that snapshot is current. For a media dashboard that streams device status into a team presence sidebar, presence accuracy matters more than shaving a few milliseconds from reconnect time.
I started with the tempting design: keep the last green dot, open a new socket, and apply whatever events arrive next. That looked fine in a notebook. In a real browser, a laptop sleeps, the network changes, and queued events can arrive after a newer snapshot. The sidebar then lies with confidence.
The experiment constraint is simple: every displayed state must have a monotonic revision, an observed-at timestamp, and a freshness deadline. Before copying this pattern, measure stale-state duration, false-online rate, and time to converge after a network drop.
Stale is a state.
The failure is a stale truth, not a closed socket
A WebSocket close event tells you transport state, not device state. A media encoder can remain healthy while the operator's browser is offline; the reverse is also possible. Keep those facts separate in the data model. The device service owns heartbeats. The browser owns whether its view is fresh.
Use a small state machine: connected, reconnecting, and unknown. On disconnect, move to reconnecting immediately, but do not mark every person or device offline. If the freshness deadline passes, render unknown and stop implying presence. This one visual rule prevents an eight-minute-old green badge from becoming an incident signal.
I once spent an afternoon chasing a supposed 1006 outage. The server was fine; the tab had resumed from sleep with revision 418 while the stream was already at 431. The error code was a clue about transport, not proof about presence.
How should browser reconnects protect presence accuracy in a team sidebar?
Give each stream a cursor and make the server offer two operations: snapshot(cursor=None) and events(after_revision). A reconnect first requests a snapshot. Only after the snapshot revision is installed should the client accept incremental events. If the server cannot replay from the requested revision, it sends a new snapshot marker.
Here is a compact Python model of that decision. It is transport-agnostic, so the same reducer can sit behind WebSocket, Server-Sent Events, or a test fixture.
from dataclasses import dataclass
from typing import Dict, Iterable, Optional
@dataclass(frozen=True)
class DeviceState:
device_id: str
online: bool
revision: int
observed_at: float
class PresenceView:
def __init__(self, stale_after: float = 15.0) -> None:
self.devices: Dict[str, DeviceState] = {}
self.revision = 0
self.phase = "unknown"
self.stale_after = stale_after
def apply_snapshot(self, states: Iterable[DeviceState], now: float) -> None:
states = list(states)
self.devices = {state.device_id: state for state in states}
self.revision = max((state.revision for state in states), default=0)
self.phase = "connected" if all(
now - state.observed_at <= self.stale_after for state in states
) else "unknown"
def apply_event(self, state: DeviceState, now: float) -> None:
if state.revision <= self.revision:
return
self.devices[state.device_id] = state
self.revision = state.revision
self.phase = (
"connected"
if now - state.observed_at <= self.stale_after
else "unknown"
)
def disconnected(self) -> None:
self.phase = "reconnecting"
def tick(self, now: float) -> None:
if any(now - state.observed_at > self.stale_after for state in self.devices.values()):
self.phase = "unknown"
The reducer rejects an old event instead of trying to guess its intent. In production, keep a per-device revision if devices publish independently; a global revision is easier when the stream coordinator assigns one. The important invariant is the same: a lower revision never overwrites a newer one.
Run the reducer against recorded traces, not hand-picked happy paths. My eval fixture has sleep/wake, duplicate delivery, reordered events, and a 12-second clock skew. One trace starts with revision 418, drops the browser connection while the device flips offline at 419, then delivers event 431 before the reconnect snapshot; another duplicates 431 three times and delays the snapshot by 9 seconds. A trace passes only when the final device map matches the authoritative snapshot, every duplicate is harmless, the cursor advances without a gap, and the view never displays connected beyond its deadline. Keeping these traces in the repository makes a reconnect change reviewable: a new reducer either preserves the invariant or shows exactly which timeline changed.
Reconnect policy should spend attempts where they help
Use exponential backoff with jitter, a cap, and a reset after a stable connection. A tight loop amplifies a regional network problem; a huge fixed delay makes a recovered operator stare at unknown. Five attempts over roughly 30 seconds is a reasonable starting hypothesis, not a universal constant. Your mileage may vary, so tune it from convergence metrics.
import random
import time
from typing import Callable
def reconnect(connect: Callable[[], None], attempts: int = 5) -> bool:
for attempt in range(attempts):
try:
connect()
return True
except OSError:
base = min(30.0, 0.5 * (2 ** attempt))
time.sleep(base + random.uniform(0, base * 0.25))
return False
Do not retry authentication failures forever. Classify close reasons and HTTP responses before scheduling the next attempt. A user-visible “sign in again” state is more accurate than a spinner that quietly burns battery. Also pause reconnect work when the document is hidden for a long period, then force a snapshot on visibility change.
What should the dashboard measure before shipping?
Presence accuracy needs product-level telemetry. Log the revision installed, snapshot age, event gap, reconnect attempt, and the reason the view entered unknown; avoid logging names or message contents. Sample a synthetic device that flips state on a known schedule, then compare the sidebar timeline with the source timeline.
A useful scorecard has four numbers: false-online seconds, false-offline seconds, p95 convergence time, and percentage of sessions that recover without a full reload. Add a fifth check for event-gap frequency. A reconnect that succeeds but skips revision 512 is not a success.
Measure it twice: once on a clean Wi-Fi link, and once after an actual sleep/wake cycle.
The catch is deliberate: this design spends an extra round trip on a snapshot and may show unknown more often than a last-known-value UI. It is not suitable when the product can tolerate stale presence and must render continuously during long offline windows. Stick with a cached last-known view for that case, label it with its age, and keep critical actions disabled until freshness returns.
For a live media dashboard, I would choose the stricter state machine because operators use the sidebar to decide whom to page or which encoder to inspect. The transport can change later; the correctness contract should not.













