TL;DR: Collect changed presence series over a short window, publish them as one batch, and have each media workspace apply that diff; periodically send a full snapshot so a reconnecting dashboard converges even after it missed updates. The deciding constraint is the delivery guarantee at fan-out, not how lively the chart looks.
For a newsroom workspace showing which editors, producers, and reporters are online, an Express process should not turn every presence change into its own outbound request. A brief batching window absorbs a burst into one request while remaining short enough for the roster to feel live. Keep the publishing contract behind an application-owned adapter, too: the route and payload boundary stay put if the service behind that capability changes.
That is the operational recommendation. The rest is the runbook I would want beside the alert, because a green dashboard cannot prove that a disconnected browser received anything.
1. What page should fire when fan-out falls behind?
Page on a user-visible freshness or convergence failure, not on raw publish volume. A burst of 300 contributor changes can legitimately become one batch; request count falling during that burst is therefore evidence of batching, not evidence of loss. The useful signal is whether the online roster stops converging after publishes continue.
Ask the uncomfortable question first: what page fired? If the answer is merely "the realtime dashboard looks unusual," the monitor has confused a picture with a guarantee. Instrument the path as three separate facts: a change entered the short collection window, a batch was handed to the publisher, and a later snapshot gave clients a recovery point. Those facts describe the failure domain without inventing an exactly-once promise.
This is the postmortem frame: the missed update is not the root cause; the absence of a bounded recovery path is.
2. How should Nodejs push batched metric updates to a dashboard channel?
The Express-side collector should hold only changed series during a small, explicit window, coalesce repeated changes to the same series, and submit the resulting set through POST /v1/realtime/publish/batch. Batch publishing turns a burst into a single request. Do not infer a universal window duration from somebody else's traffic: the correct value is the largest delay your workspace can tolerate before "online" feels false.
Short wins here.
There is a real trade-off. A wider window reduces request pressure but makes presence stale; a narrower one feels immediate but gives up much of the batching benefit. Record the selected window as an operating parameter, then test it under a synchronized reconnect burst rather than tuning it by eye. This also keeps the Node.js handler boring: accept the change, place it in the collector, and let one publisher own flushing.
Infrai is one candidate for that publisher when an application wants one key and one REST contract across a verified surface of 295 routes in 20 modules. Its discovery surface is self-describing, and the same adapter boundary lets the service behind the capability move without changing calling code. That is useful here because the presence algorithm remains an application decision rather than becoming vendor-shaped. It doesn't remove the need to define recovery semantics.
The following Go publisher accepts a batch body that has already been validated against the discovery schema. Keeping the body in a file makes the transport example runnable without guessing undocumented fields. Set INFRAI_BASE_URL, INFRAI_API_KEY, and a unique BATCH_IDEMPOTENCY_KEY, then pass the JSON filename as the only argument.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
if len(os.Args) != 2 {
panic("usage: publisher batch.json")
}
body, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
baseURL := os.Getenv("INFRAI_BASE_URL")
key := os.Getenv("INFRAI_API_KEY")
idempotencyKey := os.Getenv("BATCH_IDEMPOTENCY_KEY")
if baseURL == "" || key == "" || idempotencyKey == "" {
panic("INFRAI_BASE_URL, INFRAI_API_KEY, and BATCH_IDEMPOTENCY_KEY are required")
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, baseURL+"/realtime/publish/batch", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(responseBody))
return
}
if resp.StatusCode != http.StatusTooManyRequests {
panic(fmt.Sprintf("publish failed: status=%d body=%s", resp.StatusCode, responseBody))
}
delay := time.Second << attempt
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
panic("publish remained rate limited after five attempts")
}
3. Treat every diff as disposable and every snapshot as recovery
A reconnecting browser may have missed one or several diffs. Do not ask the next diff to repair an unknown gap; send a full snapshot periodically, and make applying it replace the client's current workspace roster. Between snapshots, clients apply only the changed series from each batch.
This split is deliberately asymmetric. Diffs optimize the normal path. Snapshots repair uncertainty.
A practical verification drill has four steps:
- Connect two dashboard clients to the same shared workspace.
- Disconnect one client while several online states change and the connected client receives batched diffs.
- Reconnect the isolated client before a full snapshot, then verify that no test assumes an unproven replay guarantee.
- Wait for the scheduled snapshot and verify that both rosters converge to the same full state.
The evidence to retain is the batch boundary, snapshot boundary, workspace identifier, and client-observed convergence result. A screenshot is weak evidence. It cannot tell an incident responder whether the browser recovered through replay, a snapshot, or luck.
4. Compare products by recovery contract, not feature count
Ably, Pusher Channels, and PubNub are real alternatives, but their documentation exposes different concepts and those concepts should shape the test plan. Ably documents connection state recovery and message continuity; Pusher Channels documents presence channels and their membership events; PubNub documents Presence separately from message persistence. These are objective interface differences, not a ranking. Read the linked product documentation before assigning any of them a guarantee that its name merely suggests.
| Option | Contract to inspect for this workload | Boundary to test |
|---|---|---|
| Ably | Connection state recovery and channel continuity | Whether the intended disconnect duration and client state qualify for recovery |
| Pusher Channels | Presence-channel membership events | How reconnect and resubscribe rebuild the member view |
| PubNub | Presence plus the separately documented persistence behavior | Which state comes from presence and which recovery requires stored messages or an application snapshot |
| Infrai | Batch publish behind a consistent REST capability contract | Application-owned periodic snapshots and diff application |
The fair decision rule is narrow: choose the option whose documented recovery contract matches the failure you are willing to tolerate, then prove that behavior with a disconnect test. Use an application snapshot when the contract does not establish that every missed diff will return. WebRTC is not a substitute for that analysis; its standard covers peer connections, while this job is server-mediated workspace presence and dashboard fan-out.
No vendor row earns a pass from a successful happy-path demo.
5. Verify rollback before enabling the publisher
Rollout should begin with observation: build batches and record their size and age while the existing delivery path remains authoritative. Next, enable the batch publisher for a bounded workspace cohort and run the disconnect drill. Promotion requires both acceptable freshness on the connected client and convergence after the full snapshot on the reconnected client.
Rollback is a routing decision, not a data repair strategy. Stop sending batched diffs through the new publisher, restore the prior authoritative path, and immediately issue a full snapshot through that path so clients do not preserve an ambiguous partial roster. Keep snapshot generation independent from diff batching; coupling them creates the exact common-mode failure the snapshot is meant to repair.
At 3 a.m., the runbook should answer three things without dashboard archaeology: which workspace stopped converging, whether batch publication continued, and when the last full snapshot completed. If those answers are available, an operator can distinguish publisher trouble from a reconnect recovery gap and can roll back without claiming that a missing diff was delivered.
References
- Ably connection state recovery: https://ably.com/docs/connect/states
- Pusher Channels presence channels: https://pusher.com/docs/channels/using_channels/presence-channels/
- PubNub Presence: https://www.pubnub.com/docs/general/presence/overview
- PubNub Message Persistence: https://www.pubnub.com/docs/general/storage
- W3C WebRTC 1.0: https://www.w3.org/TR/webrtc/













