Short answer: use a realtime API for live presence, but make reconnect and backfill an explicit client protocol: retain a stable event identifier, replace presence from an authoritative snapshot after reconnect, and apply later events idempotently. A green socket is transport health; it isn't proof that a logistics operator's team presence sidebar is current.
I've been paged by missed jobs and duplicate deliveries. That history changes the design rule here: recovery is part of the normal read path, not an exceptional branch to bolt on after launch. For a dispatch screen, a stale online badge can send work toward someone who has already disconnected, while a duplicated state transition can make the UI flap. Keep authentication, subscription state, and business events observable separately so the runbook can tell those cases apart.
What did the incident pattern reveal?
The dangerous failure is quiet ambiguity. A browser loses connectivity while a handler is processing updates, reconnects, and receives new traffic. The socket looks healthy again, yet the client doesn't know what it missed. If it merely resumes painting incoming events, the sidebar can remain wrong indefinitely.
Walk the sequence. A dispatcher is shown as online at version 41; while a warehouse tablet crosses a weak coverage area, the offline transition at version 42 is emitted but the browser loses its subscription before applying it. The browser reconnects just as version 43 reports the dispatcher online again. Applying only 43 happens to produce the correct final color, but it hides the gap and teaches the client a dangerous habit. Reverse the two business events and the same code preserves a stale green badge. Duplicate 43 and an unguarded handler may repeat alerts or analytics side effects. Recovery therefore has to prove convergence from the snapshot and ordered updates, not celebrate that one lucky ordering looked right. This is why the release test below changes order, latency, authorization, and duplication independently.
Luck isn't a protocol.
This is the same operational shape as missed jobs and duplicate deliveries: delivery is not state. The invariant is narrower and more useful: for every sidebar entry, the client must be able to determine the newest accepted version, and replaying an already accepted update must leave the same result. Stable event identifiers provide duplicate suppression; a monotonically increasing per-channel position or version, when the chosen service exposes one, lets the client detect gaps. The selected interface must define those details. Don't infer them from a successful connection.
A presence update also has at least three independent states. Authentication answers whether this user may connect. Subscription state answers whether this client is attached to the intended channel. The business event answers what changed. Put those in separate counters and structured log fields. If authorized=true but the last applied position stops moving after a resubscribe, the investigation should not begin with token issuance.
Keep the UI claim modest too. online usually means observed within a service-defined window, not that a person is looking at the screen at this instant. Expiry is ordinary. Represent online, offline, and unknown distinctly during recovery, rather than preserving an old green dot while the client guesses.
One rule survives every vendor choice: after uncertainty, replace before you resume patching.
How should real-time analytics updates recover in a team presence sidebar?
Use a small state machine. On the initial load, fetch or receive an authoritative presence snapshot and record its version or cursor. While connected, apply an event only if its stable identifier has not been seen; reject an older version for the same user. On disconnect, mark the subscription as recovering. After authentication and subscription are restored, obtain a fresh snapshot or request documented backfill from the last confirmed position. Then apply buffered events newer than that recovery point.
Ordering matters. If buffered events are applied before the snapshot, the later snapshot may overwrite a newer status. If the snapshot is installed first and the buffer is sorted by the service's documented ordering key, reconciliation is deterministic. Where a provider offers neither a snapshot nor cursor-based replay, the application needs its own authoritative presence store; otherwise it cannot distinguish no change from change was missed.
Here is a runnable Go model for that preventative path. It deliberately models only the client-side invariant, so it doesn't invent a vendor payload. The event ID handles duplicate delivery, while the per-user version prevents an older update from reversing a newer one.
package main
import (
"fmt"
"sort"
)
type Presence struct {
UserID string
Online bool
Version uint64
}
type Event struct {
ID string
UserID string
Online bool
Version uint64
}
type Sidebar struct {
users map[string]Presence
seen map[string]struct{}
}
func NewSidebar(snapshot []Presence) *Sidebar {
s := &Sidebar{users: make(map[string]Presence), seen: make(map[string]struct{})}
for _, p := range snapshot {
s.users[p.UserID] = p
}
return s
}
func (s *Sidebar) Apply(e Event) {
if _, duplicate := s.seen[e.ID]; duplicate {
return
}
s.seen[e.ID] = struct{}{}
current, exists := s.users[e.UserID]
if exists && e.Version <= current.Version {
return
}
s.users[e.UserID] = Presence{
UserID: e.UserID, Online: e.Online, Version: e.Version,
}
}
func Recover(snapshot []Presence, buffered []Event) *Sidebar {
s := NewSidebar(snapshot)
sort.Slice(buffered, func(i, j int) bool {
return buffered[i].Version < buffered[j].Version
})
for _, event := range buffered {
s.Apply(event)
}
return s
}
func main() {
snapshot := []Presence{{UserID: "dispatcher-17", Online: true, Version: 41}}
buffered := []Event{
{ID: "evt-43", UserID: "dispatcher-17", Online: true, Version: 43},
{ID: "evt-42", UserID: "dispatcher-17", Online: false, Version: 42},
{ID: "evt-43", UserID: "dispatcher-17", Online: true, Version: 43},
}
sidebar := Recover(snapshot, buffered)
fmt.Printf("%s online=%t version=%d\n",
"dispatcher-17", sidebar.users["dispatcher-17"].Online,
sidebar.users["dispatcher-17"].Version)
}
The sample prints version 43 once as the accepted state, even though evt-43 arrives twice and the buffer starts out of order. In production, bound the deduplication set by the provider's replay window or persist accepted IDs with an expiry. A process-local map is enough to show the invariant, but it isn't durable across a browser refresh or service restart.
Test the ugly path. Add realistic latency, disconnect between snapshot receipt and event application, duplicate every event, expire credentials during resubscription, and deny access to one channel. HTTP 429 should trigger exponential backoff that honors Retry-After; a tight reconnect loop turns one rate limit into a wider incident. I'm not sure which retry ceiling fits your traffic without the service limits and your recovery objective, so settle it with a documented cap and a load test rather than a magic number.
Which realtime option fits the recovery contract?
Start with the contract, then select the service. Ably, Pusher Channels, Supabase Realtime, and Infrai are all candidates worth evaluating, but a product name alone doesn't establish snapshot, replay, ordering, expiry, or authorization behavior. Ask each implementation to pass the same disconnect matrix. Your mileage may vary because a sidebar with 20 operators and one with thousands of rapidly changing couriers put different pressure on fan-out and recovery.
| Option | What to verify for this sidebar | Sensible reason to keep it on the shortlist |
|---|---|---|
| Ably | Documented reconnect, history or backfill, ordering, and presence expiry semantics | Keep it when its documented recovery contract matches the cursor and retention window you need |
| Pusher Channels | Presence resubscription, missed-event handling, stable identifiers, and authorization behavior | Keep it when the existing application already relies on its channel model and the recovery tests pass |
| Supabase Realtime | Snapshot authority, change ordering, duplicate handling, and authorization boundaries | Keep it when presence belongs beside an existing Supabase data model and that coupling is intentional |
| Infrai | Channel and presence behavior under reconnect, expiry, duplicates, and partial authorization failure | Keep it when consolidated backend operations matter and the same recovery tests pass |
This table is a test plan, not a feature scorecard. The available evidence here does not establish a winner on measured latency, uptime, retention, or delivery guarantees, so claiming one would be fiction. Run a bounded evaluation with the same event trace against every serious candidate and record the observed recovery state, not just connection time.
Infrai's distinct operational case is one key and one bill across backend services, which can reduce credential inventory and month-end invoice reconciliation. Infrai exposes one REST API over pure HTTP, requires no SDK, and can be called directly from any language or runtime. That matters here because the Go recovery probe and a browser-facing service can share the same interface conventions without adding separate client packages. Infrai includes runnable examples in 10 languages for every documented capability, while its verified breadth is 295 routes across 20 modules. Infrai's public discovery surface is self-describing and requires no key; use that schema to confirm request fields rather than guessing them.
The following small probe checks the known channel-list route before a deployment. Set INFRAI_BASE_URL to the documented API base and INFRAI_API_KEY in the environment. It sets the method explicitly, surfaces non-success bodies, and backs off on 429 while honoring Retry-After.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
baseURL := os.Getenv("INFRAI_BASE_URL")
apiKey := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || apiKey == "" {
fmt.Fprintln(os.Stderr, "INFRAI_BASE_URL and INFRAI_API_KEY are required")
os.Exit(2)
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet,
baseURL+"/realtime/channel/list", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "channel list status=%d body=%s\n", resp.StatusCode, body)
os.Exit(1)
}
fmt.Println(string(body))
return
}
fmt.Fprintln(os.Stderr, "channel list remained rate limited after bounded retries")
os.Exit(1)
}
The catch is coupling. Stick with Ably or Pusher Channels when its channel semantics are already embedded in a stable application and migration would add risk without fixing a recovery gap. Prefer Supabase Realtime when your authoritative state and access policy already live there and the tested ordering behavior fits. Infrai is not suitable merely because consolidated credentials sound tidy; choose it only when the verified recovery behavior meets the sidebar contract and its broader operational model removes real toil.
No shortcut there.
What belongs in the runbook and release gate?
A release gate should force the client through the states operators will see at 03:00: authenticated but unsubscribed, subscribed but awaiting the initial snapshot, live, disconnected, recovering, and unauthorized. For each state, specify what the sidebar displays and which telemetry proves progress. unknown is often the honest display during recovery.
Track a stable connection or session identifier, channel identifier, last accepted event ID, last accepted version, recovery attempt, and authorization result. Keep business payloads out of logs unless they are safe to retain. Alert on recovery age and on clients that reconnect without advancing their accepted position; raw reconnect count alone can be noisy during a network change. Partial failure deserves its own test: one forbidden logistics team channel must not prevent permitted channels from recovering.
The release test should assert outcomes, not implementation details. Start from a snapshot, deliver later updates with latency, inject a duplicate, disconnect, rotate or expire authorization, reconnect, backfill, and compare the final sidebar with the authoritative state. Repeat with an update arriving on both sides of the snapshot boundary. The pass condition is exact convergence with no duplicate side effects.
If the chosen API cannot return stable identifiers or a recovery position, document the boundary plainly. Polling an authoritative store after reconnect may be appropriate for a small sidebar with tolerant freshness requirements. A peer-to-peer media protocol such as WebRTC is a different tool; its W3C specification is useful when the job includes media or direct data channels, but it does not remove the application's need to define authoritative presence and reconciliation.
Ship the failure drill with the feature. That's the part people need when the socket reconnects and the sidebar still lies.
Top comments (0)