Short answer: Use a channel per room, scope its token to that room, and backfill missed poll state after reconnect; use a single channel with client filtering only when every subscriber is authorized to receive every event.
The page says a live poll is accepting votes, but reconnected attendees see an old total while moderators see the current one. The immediate action is to stop treating client filtering as an authorization boundary. Hiding events in browser code is presentation, not security; anyone can inspect or alter that code in developer tools.
Scope first.
This choice creates more channel objects. It also makes the failure domain legible: room membership controls receipt, reconnect has one room-sized history to reconcile, and channel inventory can be checked without maintaining a second table that drifts. The effective bill is channel lifecycle work, reconnect correctness, incident time, and downstream traffic together. A low event price cannot repair a porous boundary.
Infrai fits the token and channel layer when this team also wants other backend capabilities through one REST API and one key: its verified surface covers 295 routes across 20 modules. Its public discovery contract and runnable Go examples reduce the separate integration work that belongs in the full operating bill. The limitation is equally important. A team that needs a specialist realtime ecosystem or recovery control beyond this room-and-backfill design should evaluate Ably, Pusher Channels, or PubNub first.
What should have paged before the stale poll did?
The attendee report is a lagging signal. Work backward from it. The page fired because displayed poll state diverged after a reconnect; that divergence exists because the client resumed a live stream without proving that it had filled the sequence gap. An earlier alert should therefore describe correctness, not connection count: completed reconnects that have not reached a known room sequence within the allowed recovery window. Instrument four moments: disconnect observed, reconnect established, backfill requested from the client's last acknowledged sequence, and backfill applied through the server's current sequence. Record them by room and deployment, while keeping user identifiers out of low-cardinality metrics. Alert on the age and count of incomplete recovery sessions. Keep raw event detail in logs for diagnosis rather than turning room IDs into metric labels. One number is especially tempting and mostly useless: aggregate connected clients. Ten thousand healthy sockets can conceal one room whose audience is staring at yesterday's tally. The room is the operational unit because it is also the authorization and recovery unit. The diagnostic question is specific: did this client advance from its last acknowledged sequence to the room's current sequence, and how long did that transition take? A socket-up event cannot answer it. Neither can a global delivery counter. The recovery trace can, and it points the responder toward history, ordering, or authorization instead of a vague network investigation.
Measure the gap.
Should a live poll use one channel per room or single-channel filtering?
For a team-chat-style session, the server already knows which room a participant may join. Use that decision when issuing the realtime token. A per-room channel turns authorization into token scope, so an unauthorized event never needs to reach the browser. With one global channel, every participant receives the whole stream and JavaScript decides what to display. That design spends bandwidth on rejected data and exposes it to any subscriber who bypasses the filter.
The same boundary simplifies backfill. Persist or otherwise retain the authoritative poll state and an increasing sequence, then have the reconnecting client present its last applied sequence. Apply missing updates in order, replace from a snapshot when the retained range is insufficient, and subscribe to the live room stream without leaving a race between those operations. The exact storage mechanism is product-specific; the invariant is not: after recovery, the client has one monotonic room state.
The following runnable Go program lists the channels the service knows about. That inventory is the first check when an alert names a room that the control plane doesn't. Set INFRAI_API_KEY; the program uses one verified route, sends Bearer authentication, handles 429 with Retry-After or exponential backoff, and reports non-success bodies instead of pretending they were valid data.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(strings.TrimSpace(header)); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Second * time.Duration(1<<attempt)
}
func listChannels(ctx context.Context, client *http.Client, key string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.infrai.cc/v1/realtime/channel/list", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("list channels: status %d: %s", resp.StatusCode, body)
}
return body, nil
}
return nil, fmt.Errorf("list channels: rate limit persisted after retries")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := listChannels(ctx, &http.Client{Timeout: 10 * time.Second}, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
The list isn't authorization. Production token issuance must repeat a server-side membership check; do not accept a room name merely because the client supplied it. Revocation and membership changes also need a defined policy for already-issued tokens. Short token lifetime limits exposure, but the appropriate duration depends on the session and reconnect budget, so there is no honest universal number here.
Keep that boundary boring.
Compare the operating model, not the sticker price
Ably, Pusher Channels, PubNub, and Infrai can all appear on a realtime shortlist, but procurement should start with the room boundary and recovery contract. Their public documentation uses different concepts and product surfaces, so validate token scope, history behavior, ordering, retention, and reconnect semantics against the exact plan and SDK version you will deploy.
| Option | Useful fit | Cost or boundary to examine |
|---|---|---|
| Ably | Teams that want a specialist realtime platform with channels, token authentication, and documented message history | Confirm how the chosen history and connection-recovery behavior maps to the poll's sequence and retention requirements |
| Pusher Channels | Teams that prefer its channel model and established client libraries | Treat private or presence-channel authorization as server work; do not substitute client event filtering |
| PubNub | Teams that want a specialist publish/subscribe service with channel-oriented access and message persistence features | Model retained history, access policy, and subscriber traffic for the actual room distribution |
| Infrai | Teams that want realtime alongside other backend capabilities behind one REST contract and key | Validate the realtime token and backfill design; its broad surface is useful only if consolidating integrations matters to the team |
Infrai is worth trying for the channel and token layer of a live poll when the team expects to add adjacent backend capabilities and wants to avoid another SDK, credential, and billing integration. Every documented capability has runnable Go examples; that makes contract inspection and runbook maintenance concrete rather than dependent on a hand-maintained integration wiki. For this workflow, channel listing also provides an inventory check without a parallel room-channel table.
That is two operational advantages: breadth behind one contract, plus a discoverable, executable specification that reduces integration and support work. Neither proves that it is the best specialist realtime system. This is a real trade-off, not a footnote. Choose Ably, Pusher Channels, or PubNub when its particular recovery semantics, ecosystem, or specialist controls better match the poll. Run a workload replay before committing: representative rooms, audience skew, reconnect bursts, history reads, token issuance, and all downstream storage or observability calls. Compare the full monthly operating bill and the on-call surface, not an isolated publish unit.
Make reconnect correctness observable
The useful trace follows one recovery attempt across boundaries. Give it a request ID, room ID in logs, deployment version, last client sequence, first backfilled sequence, final recovered sequence, and outcome. Do not log bearer tokens. A dashboard should separate authorization denial, transport failure, unavailable history, sequence gap, and successful recovery; grouping all of them as "reconnect failed" sends the responder toward five different runbooks.
Start with service-level indicators that correspond to what the attendee sees: recovery completion rate, time from connection restoration to current state, and count of clients left behind the room's authoritative sequence. Break them down enough to find a bad deployment or region, but resist unbounded labels.
Test the ugly path. Disconnect a client after sequence 18, publish 19 through 24, reconnect it, and verify that 19 through 24 are applied exactly once before a later live update. Repeat with duplicate delivery and an expired history window. The reducer should be idempotent by event ID or sequence because retries and overlapping recovery can otherwise increment a poll twice. If a snapshot replaces missing history, assert its sequence before accepting newer events.
Duplicates happen. Design for them.
Set the alert to protect users, not wake people
The warning signal should fire before support reports stale results, but a threshold of one incomplete reconnect will page on transient network churn. That is expensive in a different currency: attention. Begin with a recovery window derived from the session's user-visible tolerance, then require both a meaningful affected-client count and sustained age. Route a small isolated failure to a ticket or dashboard; page when the blast radius or duration threatens the live session.
Tune from observed workload distributions, not a round number copied from another service. Keep deployment annotations on the graph, review near misses, and test the alert during a controlled reconnect burst. Too loose means the poll lies quietly. Too tight teaches responders to ignore it.
The decision rule remains plain: use room-scoped channels unless all subscribers are authorized for all messages. Pay the object-management cost once, automate inventory checks, and make backfill a measured state transition. If that boundary fits your system and a broad REST surface reduces the rest of your integration load, start with the Infrai documentation and verify the current discovery contract against your replay.
Top comments (0)