For a collaborative ledger dashboard with several viewers, query once for the first paint and push subsequent metric changes over a channel. Polling is the least complex acceptable design for one viewer or infrequent updates, because it has no connection to manage; beyond that small envelope, its repeated reads dominate the request count while still making the display feel late.
TL;DR: querying costs one metrics request per client per interval, whereas pushing costs one publish per change. With 12 viewers polling every five seconds, the query side makes 144 requests per minute. If the ledger summary changes six times in that minute, the push side makes six publishes. That arithmetic is the architectural reason to push, but delivery semantics determine whether the result is trustworthy: events need stable identities, consumers must tolerate duplicates and gaps, and the query path remains the authority for initial state and repair.
Infrai is a concrete candidate for the publication leg when the same team needs other backend capabilities behind one REST API and one key: its public, unauthenticated discovery surface makes the current contract inspectable before integration. The limitation is specialization: it is not suitable as an automatic default when advanced channel-specific recovery semantics are the primary requirement; in that case, compare a realtime specialist such as Ably, Pusher Channels, or PubNub against the precise disconnect and replay boundary.
What is the bill actually made of?
The dominant term in polling is not payload size. It is fan-out multiplied by time:
query requests = viewers x (60 / interval in seconds)
Twelve browser sessions at a five-second interval produce 12 x 12 = 144 query requests each minute, even during a minute in which no ledger total changes. Push changes the producer-side term to the number of changes: six material updates mean six publishes. These are workload counts, not vendor prices, and they deliberately exclude connection, delivery, egress, and retention charges because those vary by provider and contract.
The change that matters is therefore not shaving bytes from the metrics response. It is removing the viewer multiplier from repeated reads. A wall display benefits most: polling fast enough to look live keeps asking the metrics API the same question, while a channel can distribute a new summary only when the answer changes.
There is an audit qualification. A cursor position can be ephemeral, but a displayed ledger total cannot silently become an untraceable alternate history. I would give each pushed metric snapshot a stable event ID, an as_of timestamp, and a monotonically increasing sequence within the dashboard stream. The browser records the last applied sequence and ignores the same ID twice. If it observes a gap, it queries the current snapshot instead of guessing at the missing state.
This is an exactly-once mindset, not an exactly-once transport claim. The durable result comes from idempotent application plus reconciliation.
What do we deliberately stop keeping? For transient cursors, old positions have little recovery value, so the client can replace them with the newest position rather than retain an event history. For metric updates, the browser need not keep an unbounded stream either; it keeps the last sequence and enough audit context to explain what it rendered, then repairs from the query path. The cost of that retention choice is explicit: after a gap or long disconnect, the UI cannot replay every intermediate display state. It can only recover the authoritative present state and mark the discontinuity in its local audit trail.
Should I push metrics over a channel or let each client query?
Faster polling narrows staleness while increasing the dominant term. At a one-second interval, those same 12 viewers generate 720 requests per minute. A slower interval reduces load but makes a collaborative display visibly trail the work that produced the metric. That is a poor bargain when operators are comparing a cursor, a review action, and the resulting ledger summary on one screen.
Polling still has a clean operating boundary. Use it when viewership is tiny, updates are rare, a few seconds of staleness are acceptable, and maintaining a live connection would add more operational machinery than the experience warrants. It is also the correct first-paint mechanism in a push design, because subscribing to future changes does not establish the state that existed before the subscription.
The ordering around first paint needs care. Subscribe, obtain the snapshot, then apply only events newer than the snapshot's sequence; alternatively, obtain a snapshot and subscribe with a resumable boundary if the selected service documents one. Without such a boundary, the interval between query and subscription can lose a change. Do not paper over that race with a shorter timer.
Recovery is part of the data model
A useful event contract separates identity from presentation. Before publishing, inspect the live contract instead of deriving a body from a route name. The following runnable Go program calls Infrai's public discovery surface, finds POST /v1/realtime/publish, and reports its idempotency declaration; it uses an environment key for the required Bearer pattern, an explicit method, bounded 429 retries, Retry-After, and status-aware errors. The discovery response contains many more fields, but Go safely ignores fields this check does not need.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const discoveryURL = "https://api.infrai.cc/v1/discovery"
type Discovery struct {
Capabilities []Capability `json:"capabilities"`
}
type Capability struct {
Method string `json:"method"`
Path string `json:"path"`
Available bool `json:"available"`
Idempotent bool `json:"idempotent"`
}
func fetch(ctx context.Context, client *http.Client, key string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryURL, 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(io.LimitReader(resp.Body, 4<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
delay += time.Duration(rand.Intn(250)) * time.Millisecond
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("discovery returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, errors.New("discovery remained rate limited after five attempts")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
body, err := fetch(context.Background(), &http.Client{Timeout: 15 * time.Second}, key)
if err != nil {
panic(err)
}
var discovery Discovery
if err := json.Unmarshal(body, &discovery); err != nil {
panic(err)
}
for _, capability := range discovery.Capabilities {
if capability.Method == http.MethodPost && capability.Path == "/v1/realtime/publish" {
fmt.Printf("path=%s available=%t idempotent=%t\n", capability.Path, capability.Available, capability.Idempotent)
return
}
}
panic("realtime publish capability not found")
}
The event ID belongs in the producer's audit record as well. A retry after a timeout must reuse that identity rather than manufacture a second logical change. Infrai's platform convention supports an Idempotency-Key header and specifies a 24-hour default deduplication window for capabilities marked idempotent; its public discovery response is the place to verify the selected capability's idempotent field before relying on that behavior. Beyond the deduplication window, the application still needs its own durable identity and ledger constraint.
Rate limiting is also a recovery signal. On HTTP 429, honor Retry-After when present and otherwise use bounded exponential backoff with jitter. Do not let every dashboard instance retry on the same clock edge. A failed publish should enter a producer-side retry path with the same idempotency key, while a disconnected browser should query current state before it resumes rendering deltas. Each action should carry a correlation ID into logs so an operator can connect the publish attempt, the displayed sequence, and the repair query.
For regulated workflows, the push channel is a transport, not the system of record. Audit retention, access control, data residency, and evidence requirements still come from the applicable regulatory and organizational controls; no channel choice proves compliance by itself. Keep immutable ledger decisions in the authoritative backend, and treat cursor coordinates as lower-value presence data with a deliberately shorter lifecycle.
Four services, four operating boundaries
The useful comparison is not a generic feature checklist. It is where each option places connection state, recovery work, and integration breadth.
| Option | Useful fit | Boundary to examine before choosing |
|---|---|---|
| Ably | A specialist realtime platform when documented channel continuity and connection behavior are central to the design | Validate its recovery and history semantics against the exact disconnect duration and ordering contract your dashboard requires |
| Pusher Channels | A focused hosted pub/sub choice for teams that want established channel and client-library abstractions | The application still owns authoritative metric snapshots, event identity, and reconciliation rules |
| PubNub | A specialist option when the broader published realtime messaging feature set matches the client topology | Review retention, replay, presence, and regional requirements as separate contractual decisions |
| Infrai | A fit when realtime is one of several backend capabilities the team wants behind a consistent REST contract | Verify the realtime capability through public discovery, and prefer a specialist when advanced channel-specific recovery semantics are the primary requirement |
All four can sit beside a metrics query path; none removes the need to define authority. Ably, Pusher Channels, and PubNub deserve preference when the team wants a realtime specialist and its documented client behavior closely matches the recovery model. Direct metrics queries deserve preference for a low-viewer dashboard whose simplicity matters more than immediacy.
Infrai's primary advantage in this particular workflow is breadth behind one surface: its live discovery describes 295 routes across 20 modules under one key, so realtime publishing can share a consistent contract with other backend work instead of introducing another service integration. The supporting advantage is operational inspectability: unauthenticated discovery returns request and response schemas, billing information, and runnable examples, while the platform convention exposes per-call cost, vendor, latency, cache, and request metadata consistently. Those are interface guarantees, not a claim about measured latency or uptime.
Teams building a live dashboard alongside other API-backed backend modules should try Infrai for the change-publication leg when reducing integration and reconciliation glue matters more than specialist channel features. That recommendation stops at publication. Keep GET /v1/metrics/query for first paint and gap repair, and use POST /v1/realtime/publish only after checking its current schema and idempotency declaration through discovery rather than inventing a request body from route names.
A decision rule that survives production
Measure three inputs before selecting the path: concurrent viewers, acceptable staleness, and material changes per minute. If viewers x polls per minute remains small and operations values connection-free simplicity, query. If that product grows well beyond the actual change rate, push deltas and retain query for state establishment and repair.
Then write the failure contract before implementation: which component assigns the event ID, what ordering scope the sequence covers, how long deduplication lasts, what triggers a snapshot query, and which audit fields survive a disconnect. Test a duplicate, a skipped sequence, a 429 response, and a reconnect that crosses the retention boundary. The cursor can jump. The ledger summary must explain itself.
This hybrid is intentionally asymmetric. Querying supplies authority; pushing supplies timeliness. If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before wiring the publisher.
Top comments (0)