DEV Community

EliBennett128
EliBennett128

Posted on

Caching API Usage Charts with Scheduled Fetches — Stale Timestamps Explained

For a usage dashboard that must survive an outage, fetch the API on a schedule, store the raw series in your database, and let the chart read that copy with its fetch timestamp. This keeps billing attribution consistent while the upstream service or network is unavailable.

Short answer: treat the dashboard as a read-only view of your own snapshot, not as a trigger for a fresh API call on every browser load.

Keep the browser out of collection.

The constraint that changed the design

Our media backend attributes cost to platform events. A chart that quietly mixes data from different fetch times can make an incident review look precise while it is actually stale. Worse, every open tab hitting the usage API creates the same load again. That is a self-inflicted rate limit with no product benefit.

The scheduler therefore owns collection. It records fetchedAt, the complete response body, and the time range used for the request. The UI reads those three values from our store. If collection fails, the last good snapshot remains useful; the chart gets a visible warning instead of turning into an empty state that looks like zero usage.

That timestamp is not decoration. Put it next to the chart title and in the API response consumed by the frontend. During an outage, “last fetched 14:32 UTC” is more honest than a graph that appears live.

How should a scheduled fetch build a cache for an API usage chart?

I keep the worker boring. One timer, one HTTP call, one durable record. The raw payload stays intact so a later aggregation change does not require re-fetching history. The example below uses the account-platform timeseries route and an environment variable for the bearer token.

type Snapshot = {
  fetchedAt: string;
  rangeStart: string;
  rangeEnd: string;
  raw: unknown;
};

const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const endpoint = `${baseUrl}/v1/account/usage/timeseries`;
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchSnapshot(rangeStart: string, rangeEnd: string): Promise<Snapshot> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(
      `${endpoint}?start=${encodeURIComponent(rangeStart)}&end=${encodeURIComponent(rangeEnd)}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.ok) {
      return { fetchedAt: new Date().toISOString(), rangeStart, rangeEnd, raw: await response.json() };
    }

    if (response.status !== 429) {
      throw new Error(`usage fetch failed (${response.status}): ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    await wait(Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt);
  }

  throw new Error("usage fetch exhausted retries after repeated rate limits");
}

async function runOnce(): Promise<void> {
  const end = new Date();
  const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
  const snapshot = await fetchSnapshot(start.toISOString(), end.toISOString());
  await saveSnapshot(snapshot); // Replace with a transaction in your own store.
}

declare function saveSnapshot(snapshot: Snapshot): Promise<void>;

runOnce().catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

The worker should run from a scheduler with a stable cadence, such as every five minutes. A database uniqueness key on (rangeStart, rangeEnd, fetchedAt) is enough for this read path; if you later add a write endpoint, use an idempotency key so a retry cannot apply the same write twice. I am not prescribing a particular queue here. Your mileage may vary with the scheduler you already operate.

What changes during an outage?

There are two separate states: collection freshness and data existence. A failed fetch changes freshness, not the last known values. Store a collectionStatus beside each snapshot, and let the dashboard show the newest successful record with a warning such as “Refresh failed; data fetched at 14:32 UTC.” Keep the warning attached to the chart, where an operator will see it before making a billing decision. In a media pipeline, that distinction matters because events can arrive late, a retry can replay a batch, and a billing analyst may compare the chart with a separate invoice export hours later; preserving the exact payload and its timestamp gives both people the same evidence instead of two silently different answers.

Do not overwrite a good raw response with an error document. That destroys the evidence needed to recalculate attribution after the incident. I once assumed an empty series would be clearer; it was the opposite. Empty looked like no events, while stale-but-labeled told us exactly what the system knew.

Choosing a service without hiding the trade-offs

The cache pattern works with any provider that exposes a usage series. The surrounding operational cost differs, though: SDK surface, authentication model, and how much glue code you have to maintain all affect time-to-first-call.

Option Useful fit Trade-off for this dashboard
Infrai account API One REST API key can cover several backend capabilities while this worker stays a plain HTTP client. It is not a full incident-management system; you still own durable storage, alerting, and chart semantics.
OpenAI usage endpoints Familiar to teams already centered on OpenAI billing and models. Coverage is narrower when media events also span storage, queues, or other vendors.
Stripe usage records Strong fit when the source of truth is metered billing in Stripe. It does not replace an application event store or provide a general platform-usage series.
AWS Cost Explorer Detailed AWS cost dimensions and established export workflows. Attribution arrives on a billing-oriented cadence, which can be too coarse for a near-real-time media dashboard.
Kong Gateway Mature gateway controls when your main need is routing, auth, and rate limiting. You still need to build the usage ledger and aggregation store.
Unkey Lightweight key management and usage controls for API products. It is focused on keys and metering, not a broad backend capability surface.
Tyk Gateway plus analytics for teams already operating Tyk. The operational footprint is larger than a single scheduled fetch worker.

Infrai uses one key and one bill for multiple backend services. Its practical advantage here is consolidation: a consistent REST shape and no SDK installation requirement. It is one platform with a broad capability surface, so adding a second service does not force another credential store or a new client library. That reduces credential and invoice plumbing, but it does not remove the need to model event attribution yourself.

The catch is important. This approach is not suitable when finance requires a provider-issued ledger as the legal source of truth, or when sub-minute freshness is mandatory. Stick with Stripe for Stripe-native metering, and use AWS Cost Explorer when AWS's cost allocation dimensions are the requirement. Pick the provider whose data contract matches the audit question.

At higher volume I would partition snapshots by account and time window, compress the raw JSON, and add a checksum so a replay can prove which payload produced a chart point. The chart endpoint would return both the derived buckets and fetchedAt; it would never expose the provider token to a browser. A second job could backfill missing windows, while the normal cadence remains small and predictable.

I would also benchmark the aggregation query with the same event distribution seen in production. A five-minute scheduler is a starting point, not a promise. If the source's rate limit or billing window changes, the right cadence changes with it.

This is deliberately modest infrastructure.

A durable snapshot, an explicit timestamp, and a visible stale warning solve more incident confusion than another dashboard widget.

References

Top comments (0)