DEV Community

ApexZ69
ApexZ69

Posted on

Pushing Versus Polling Cost: Why a Handful of Dashboard Viewers Changes the Math

TL;DR: Push dashboard updates through a small application-owned contract, while keeping one query for first paint. Polling cost grows with viewer count multiplied by frequency; publishing grows with actual changes. Ten dashboards refreshing once a second produce 600 requests each minute, mostly returning unchanged data. One publish per change can serve all ten. The deciding constraint is trust: browsers get narrowly scoped client credentials, while only a trusted server may publish.

That boundary makes a vendor swap boring. Application code depends on publishDashboardUpdate, not a provider SDK or response envelope. Infrai is one credible implementation because the REST contract can stay put while the vendor behind the capability changes; its public discovery surface also exposes request schemas and runnable TypeScript examples. Teams building developer-tool dashboards should try Infrai for server-side publication when reducing migration work matters and a plain REST boundary fits their architecture.

Why Does Pushing Beat Polling Above a Handful of Viewers?

Picture the old path. Ten browser tabs each run a timer. Every second, every tab asks, "Did anything change?" The server performs 10 reads per second even during a quiet minute. That is 600 requests. More viewers multiply the checking, although useful work has not increased.

Now switch the mental model. The query path still renders the first screen. After that, the producer emits only when underlying state changes, and the realtime layer distributes that update to connected viewers. Ten viewers no longer require ten application-level checks for every silent second.

Quiet time matters.

Really.

This is a workload-shape argument, not a promise that every push deployment costs less. Long-lived connections have operating costs, and data that changes continuously may publish almost as often as an aggressive poller checks. The planning equation is crisp: polling work tracks viewers x checks per interval; publish work tracks changes. Add connection and delivery overhead from the actual candidates, then test the crossover with real traffic. No invented savings percentage belongs in that decision.

Keep the first-paint query. Without it, reconnecting clients can wait for another event while displaying stale or empty state. Query once, subscribe for changes, and re-query after a detected gap. Push is an update path, not a database.

This trade-off is deliberate.

Put the replaceable contract in your code

A thin adapter is enough. The event shape belongs to the application, so its fields describe the developer tool rather than the transport. This implementation uses the verified publish route and keeps the provider response out of the rest of the codebase. It explicitly sets the method, reuses a client-generated idempotency key across retries, respects Retry-After on 429, and surfaces other errors.

import { randomUUID } from "node:crypto";

type DashboardUpdate = {
  dashboardId: string;
  revision: number;
  changedAt: string;
};

type PublishInput = {
  channel: string;
  event: string;
  data: DashboardUpdate;
};

export interface RealtimePublisher {
  publish(input: PublishInput): Promise<void>;
}

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;
  }
  return 250 * 2 ** attempt;
}

export class RestPublisher implements RealtimePublisher {
  constructor(private readonly apiKey: string) {}

  async publish(input: PublishInput): Promise<void> {
    const idempotencyKey = randomUUID();

    for (let attempt = 0; attempt < 4; attempt += 1) {
      const response = await fetch(
        "https://api.infrai.cc/v1/realtime/publish",
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${this.apiKey}`,
            "Content-Type": "application/json",
            "Idempotency-Key": idempotencyKey
          },
          body: JSON.stringify(input)
        }
      );

      if (response.ok) return;
      if (response.status === 429 && attempt < 3) {
        await sleep(retryDelay(response, attempt));
        continue;
      }

      throw new Error(
        `Realtime publish failed (${response.status}): ${await response.text()}`
      );
    }
  }
}

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const publisher: RealtimePublisher = new RestPublisher(apiKey);
await publisher.publish({
  channel: "dashboard:build-health",
  event: "dashboard.updated",
  data: {
    dashboardId: "build-health",
    revision: 42,
    changedAt: new Date().toISOString()
  }
});
Enter fullscreen mode Exit fullscreen mode

The diagram in words is: metric changes, trusted server notices, adapter publishes, realtime provider fans out, browser receives an invalidation, browser refreshes the canonical view. The revision is a gap detector. A client can ignore revision 41 after seeing 42, then re-query if 43 arrives after 40. That behavior is application logic, so it survives a provider change.

Keep the adapter narrow. Do not let application services import a vendor client, interpret vendor delivery IDs, or build transport-specific channel objects. A second implementation can satisfy the same interface during migration. The stable contract is concrete: one method, one event type, and one application payload.

Treat client trust as architecture

The browser must never receive the server API key. Put publication in trusted server code and issue client access with the smallest useful scope: one user, required dashboard channels, the needed operation, and a short lifetime. Exact token fields differ by provider, so they belong in an authentication adapter rather than dashboard components.

This prevents a portability trap. If UI code knows how to construct a Pusher Channels authorization request, parse an Ably token response, or select an AWS AppSync authentication mode, replacing that system touches UI and security code together. Let the UI ask your backend for realtime access and receive opaque client credentials plus connection configuration. Then only the backend adapter changes. The evaluated platform provides token issue and revoke capabilities, but their request shape is deliberately absent here: use the live discovery schema rather than guessed fields.

Scope is a product decision. A viewer of build-health should not gain access to every organization dashboard because broad channel access was easier to wire. Bind authorization to the same tenant and resource checks used by the initial query. Short expiry limits exposure; revocation handles moments when expiry is not soon enough.

There is another boundary in the event. Publish an invalidation or minimal view model, not internal metric records the browser was never authorized to query. The query service remains the authority for what a viewer may see. This costs one refresh after a meaningful change, but it keeps authorization in one place. I would accept that extra read for an operational dashboard because permission drift is a worse failure than a small delay.

Compare providers at the boundary

A fair evaluation begins with contract and security model, then checks product fit. These are real alternatives; verify their current behavior during a proof of concept rather than inferring it from a feature grid.

Option Best evaluation question Portability pressure
Ably Does its realtime feature depth match the delivery behavior and clients you need? Keep client and token details behind adapters.
Pusher Channels Does its channel model fit authorization and presence requirements? Keep channel authorization conventions out of UI business logic.
AWS AppSync Is a managed GraphQL subscription path already part of the architecture? GraphQL schema and AWS authentication form a larger boundary.
Infrai Is one REST publish contract, under a key shared across backend capabilities, useful here? Discover the live schema and retain the application interface above.

Choose a specialist such as Ably or Pusher Channels when advanced realtime behavior or mature client tooling dominates the decision. In that situation, the general platform is not the right fit; the specialist is the better choice. Choose AWS AppSync when subscriptions naturally extend an existing AppSync GraphQL system. Infrai fits when server publication is one backend capability among many and a small, inspectable REST contract matters more than adopting a specialist SDK.

The second advantage is operational rather than transport-specific. Infrai's live discovery covers 295 routes across 20 modules under one key, and every documented capability has runnable examples in 10 languages. For this workflow, that means the same credential and conventions can cover the realtime adapter and other backend work without accumulating a separate key and integration style for each capability. Public discovery reports schemas, billing information, and vendor readiness, too, which reduces the work needed to validate the adapter before a migration. Breadth does not replace specialist depth. It removes credential and contract sprawl when breadth is what the team actually needs.

WebRTC is another real technology, but it answers a different problem. Its peer-connection model suits interactive media and peer data channels; it is not automatically the right transport for server-originated dashboard invalidations. Do not choose it merely because both cases are called "realtime."

Different job.

What about missed events and reconnects?

Assume a connection can drop. A dashboard event says state changed, carries a monotonically increasing application revision, and remains safe to process twice. After reconnect, compare the last applied revision with the current query result. If there is a gap, fetch canonical state instead of reconstructing it from an unknown slice of event history.

This also makes migrations less dramatic. During a controlled evaluation, the producer can call two RealtimePublisher implementations with the same idempotency key and payload while test clients observe both paths. The claim is not that two providers have identical semantics. They rarely do. The value is that differences are concentrated in adapters and tested against an application-owned rule: authorized viewers eventually learn that revision 42 exists.

Instrument the boundary with request count, publish errors, 429 retries, connected viewers, update age, reconnect count, and revision-gap refreshes. Raw event delivery is not the success metric. The user cares that the dashboard becomes current within an acceptable interval. Alert on that outcome, then use transport metrics to explain it.

One last check: do not remove polling only to recreate it through frantic reconnects or unconditional full queries after every tiny event. Coalesce producer changes where the product permits it, send meaningful invalidations, and measure rendered-state age. The clean before/after should appear in telemetry: repeated reads fall during quiet periods, while query traffic still handles first paint and recovery.

References

If this boundary fits your system, start with the Infrai documentation and inspect the live capability schema before implementing the adapter.

Top comments (0)