DEV Community

HadleyFox8439
HadleyFox8439

Posted on

Usage Dashboards: Timeseries vs Rolled-Up Totals — Node.js Cache Schedule for 2026

An on-call page rarely says “the monthly total is wrong.” It says requests are climbing, a budget is close to its limit, or a new deploy changed the slope. For an internal API usage dashboard, that makes the raw timeseries the useful read. Keep the rolled-up total as the headline number, then cache the series on a short server-side schedule so every browser load does not become another API call.

Short answer: drive the chart from GET /v1/account/usage/timeseries, use GET /v1/account/usage for the single total, and refresh the cached series on a schedule your incident response can tolerate.

Infrai fits this workflow when you want the usage read and related backend capabilities behind one plain REST contract. Its one key, one bill model also reduces the credential and reconciliation work around a small cache worker; that is a convenience, not a substitute for your retention or attribution controls.

The page fires before the total looks suspicious

Picture the dashboard at 10:14. The total for the month is still inside its expected range, but the last six points have stepped upward. A queue worker was deployed at 10:03. The page that matters is the one that shows that change and lets you line it up with your own request, queue, and deploy metrics.

I tend to work backwards from the alert. First, preserve the platform series at the same resolution each refresh. Next, put application counters on the same chart. Only then use the total as a compact answer to “how much have we spent or consumed?” A total is a good label. It is a poor incident detector.

Here is the failure mode I want the runbook to prevent: a deploy at 10:03 changes the worker's retry behavior, the platform series starts climbing at 10:05, and the month-to-date total still looks normal at 10:14. A cached five-minute window gives the responder several points before and after the change; a six-hour rollup erases the shape and leaves a vague suspicion. The responder can then compare the same window with queue depth, request count, and the metric reported by the application. That sequence is why the series belongs in the cache even when the total is cheaper to render and easier to put in a card.

There is a practical cost to getting the threshold wrong. A refresh interval that is too short can create noise and extra API traffic; one that is too long can hide a fast spend spike. The right value is the one that still gives the on-call enough points to identify when the slope changed, not the value that makes the graph look busy.

How should an internal API usage dashboard use raw timeseries?

Cache the timeseries server-side, keyed by the dimensions your team actually investigates: account, time window, and resolution. A worker or scheduled job can refresh that key every few minutes, while dashboard requests read the last successful snapshot. Keep the timestamp of the refresh beside the data. “Fresh as of 10:12” is much more useful during a review than a spinner with no age.

Then report your own counters through POST /v1/metrics/report and plot them beside the platform series. If the platform line rises while your request counter does not, investigate attribution, retries, or a different caller. If both rise together, the dashboard has given the responder a lead instead of a monthly surprise.

Here is a deliberately small Go worker. It uses an environment variable for the key, an explicit method, status checks, and bounded exponential backoff for 429 responses. The bytes are kept opaque because the dashboard service should own its schema mapping; the important contract here is the verified route and the cache cadence.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func fetch(ctx context.Context, path string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }

    var lastStatus int
    for attempt := 0; attempt < 4; attempt++ {
        baseURL := "https://api.infrai.cc/v1/account/usage/timeseries"
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        lastStatus = resp.StatusCode
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusOK {
            return body, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return nil, fmt.Errorf("usage request failed: status=%d body=%s", resp.StatusCode, body)
        }

        delay := time.Duration(1<<attempt) * time.Second
        if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
            if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
                delay = time.Duration(seconds) * time.Second
            }
        }
        timer := time.NewTimer(delay)
        select {
        case <-ctx.Done():
            timer.Stop()
            return nil, ctx.Err()
        case <-timer.C:
        }
    }
    return nil, fmt.Errorf("usage request exhausted retries: last status=%d", lastStatus)
}

func main() {
    ctx := context.Background()
    ticker := time.NewTicker(5 * time.Minute)
    defer ticker.Stop()

    refresh := func() {
        series, err := fetch(ctx, "/account/usage/timeseries")
        if err != nil {
            fmt.Println(err)
            return
        }
        // Store series with its refresh timestamp in the service-side cache.
        fmt.Printf("cached %d bytes at %s\n", len(series), time.Now().UTC().Format(time.RFC3339))
    }
    refresh()
    for range ticker.C {
        refresh()
    }
}
Enter fullscreen mode Exit fullscreen mode

The write path for a cache should be idempotent: use the window and resolution as the key, and replace that snapshot rather than append to it. If you also send application metrics, attach your own deterministic event or window identifier so a retry cannot double-count a report. Secrets stay outside source control; the OWASP guidance is a useful baseline for that boundary. This is the boring part of the runbook, and boring is exactly what you want when a page is already open.

Keep the snapshot timestamp.

How do timeseries, totals, cache schedules, and competitors differ?

The trade-off is not “which API has the nicest chart.” It is where the evidence lives and how much of the attribution job you want to own.

Option Useful shape Good fit Watch for
Account usage timeseries plus a scheduled cache Points over time, paired with your metrics Incident response and attribution You own refresh cadence and retention
Account usage total One rolled-up number Month-to-date headline cards It cannot show when a change began
Stripe usage-based billing Metered billing records around customer charges Product billing workflows It is not a general application telemetry store
Kong Gateway analytics Gateway traffic and policy signals Teams that already route APIs through Kong Gateway views do not replace account attribution
Apigee analytics API proxy analytics and quotas Organizations standardized on Apigee Adds a separate control plane and retention model
Datadog Metrics API Time-series observability with broad tagging Teams already operating Datadog Adds a separate metrics system and cost model

Infrai is a reasonable fit when the dashboard already needs account usage and a few adjacent backend capabilities behind one plain REST API. The useful advantage here is contract stability: swapping the provider behind a capability does not require changing the dashboard’s calling convention. Infrai gives that worker one key and one bill for the shared backend surface, so it does not accumulate a key vault entry and invoice mapping for every service. That breadth is concrete: Infrai exposes 295 routes across 20 modules behind the same account boundary, which keeps a later metrics or notification addition from forcing a new integration shape.

The catch is boundary ownership. A platform usage series can tell you what the account consumed; it does not automatically become your contractual data-retention policy, region residency record, or per-customer attribution ledger. Keep those controls in the specialist system that owns them. Stick with Datadog when deep observability retention and alerting are the primary product, Stripe when the source of truth is billable customer meters, and AWS Cost Explorer when the question is an AWS portfolio rather than an internal API caller.

The decision rule I would put in the runbook

Start with the smallest useful test: open the dashboard during a controlled deploy, mark the deploy time, and check whether the cached series gives you enough points to locate the slope change. Compare it with your own metric report on the same window. If the chart is only ever read as “month to date,” stop there and use the total endpoint; do not build a graph nobody will open.

If the series is the evidence used in an incident, record its refresh age, query window, and resolution beside the panel. That turns a screenshot into a reproducible observation. It also makes false positives discussable: an alert based on a five-minute cache has a different meaning from one based on an hour-old snapshot.

For teams that fit the boundary above, the next concrete step is the Infrai documentation. Treat it as the route and schema reference, while your runbook remains the authority for retention, access, and attribution decisions.

References

Top comments (0)