DEV Community

PhilemonShaw8453
PhilemonShaw8453

Posted on

Usage Dashboard Timeseries vs Rolled-Up Totals: Safe Operations Explained

Short answer: drive an e-commerce usage dashboard from the raw timeseries read, and keep the single rolled-up total as a headline number. The shape of usage tells you when a spend ceiling or traffic refusal is approaching; a total alone cannot tell you when it started.

This is an operational choice, not a charting preference. During an incident, “how much?” is useful, but “since when?” is the question that separates a deploy, a retry storm, and a vendor-side change. I would cache the series on a short schedule, then let dashboard requests read that cache. Don't make every browser refresh pay the upstream latency or consume another quota unit.

Keep one small truth visible: cache age.

How should an internal API usage dashboard use raw timeseries?

Start with a time-bucketed series and put the current total beside it. A steep slope is an alert candidate even when the month-to-date number still looks ordinary. A flat line with a sudden gap is a collection problem, not evidence that traffic stopped.

The cache schedule should follow the decision window. For a page used to watch a five-minute incident, a one-minute refresh is reasonable; for a monthly finance page, fifteen minutes may be enough. Your mileage may vary, because the right interval depends on the provider's freshness guarantee and your own SLO. Record the fetch timestamp and the source window with the data so an operator can see staleness instead of mistaking it for zero usage.

I once treated a total as the canonical value in a review. It passed the demo, then a retry loop doubled calls for 18 minutes and the total only showed the damage after the fact. The series exposed the exact step change. That was the correction: totals are summaries; timeseries are evidence.

A small Go cache for scheduled reads

The following worker keeps the upstream reads server-side. It uses the two account usage routes that matter here, sets an explicit method, and preserves the response body for the dashboard. In production, put the key in a secret manager and expose only the cached result to browsers; OWASP's guidance is a good baseline for that boundary.

package main

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

type cache struct {
    mu sync.RWMutex
    series, total []byte
    updated       time.Time
}

func fetch(ctx context.Context, path string) ([]byte, error) {
    baseURL := os.Getenv("INFRAI_BASE_URL")
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
    if err != nil { return nil, err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return nil, err }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode == http.StatusTooManyRequests {
        return nil, fmt.Errorf("rate limited; retry on the next schedule")
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return nil, fmt.Errorf("usage read failed: %s: %s", resp.Status, body)
    }
    return body, nil
}

func refresh(ctx context.Context, c *cache) error {
    series, err := fetch(ctx, "/account/usage/timeseries")
    if err != nil { return err }
    total, err := fetch(ctx, "/account/usage")
    if err != nil { return err }
    c.mu.Lock()
    c.series, c.total, c.updated = series, total, time.Now().UTC()
    c.mu.Unlock()
    return nil
}

func main() {
    c := &cache{}
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()
    for {
        ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
        if err := refresh(ctx, c); err != nil { fmt.Println(err) }
        cancel()
        <-ticker.C
    }
}
Enter fullscreen mode Exit fullscreen mode

A 429 should result in backoff, not a tight loop. The example defers the next scheduled attempt; a production worker can honor Retry-After and use exponential backoff while retaining the last known good cache. Never replace good data with an empty response after a failed refresh.

The longer-term trap is retention. A 60-second bucket kept for 90 days is a different storage bill and query shape from a 15-minute bucket kept for a year. Decide which resolution supports your incident SLO, downsample only after that window, and test the resulting chart with a known burst. A useful check is to inject a synthetic 18-minute spike in a staging account, confirm that the slope is visible, then verify that the headline total reconciles after the provider's aggregation delay. If those checks disagree, document the freshness boundary instead of inventing precision.

How do Node.js schedules, caches, and metrics fit the same decision?

The dashboard frontend can be Node.js even if the collector is another service. Keep scheduling in one backend process (or a durable job runner), and have HTTP handlers serve the latest cache with an explicit updated_at. Pair the platform series with application counters such as checkout attempts, queue retries, and HTTP 429s; plotting both on a common time axis makes a provider spike distinguishable from your own traffic spike.

Do not infer causality from aligned lines. Mark deploys, budget changes, and credential rotations as annotations, and preserve raw buckets long enough to investigate your SLO window. If the only question you ever ask is month-to-date spend, the totals read is genuinely enough. Don't build a chart nobody opens.

Which managed option fits the operational boundary?

There is no universal winner. The right choice depends on who owns retention, alerting, and the spend ceiling.

Option Strength Trade-off Best fit
AWS Cost Explorer / CloudWatch Deep AWS integration and familiar IAM controls Cross-provider views and fine-grained usage can require more wiring AWS-first teams with existing account governance
Datadog Fast dashboards, monitors, and correlation across services Metering and retention choices can make spend harder to predict Teams buying a broad hosted observability workflow
Grafana + Prometheus Flexible queries and self-hosted control You operate storage, upgrades, and durability Platform teams with a clear on-call budget
Stripe Billing Strong invoice and subscription primitives Not a general infrastructure usage timeseries store Product teams whose source of truth is customer billing
Unkey Focused API-key lifecycle and limits Narrower scope than a full observability platform Teams primarily managing key quotas
Kong Gateway Gateway policies, plugins, and traffic controls Requires operating gateway configuration and telemetry Organizations standardizing on an API gateway
Infrai account usage One REST contract and one key across backend capabilities; the contract stays stable while the provider behind it changes It is not a replacement for a full alerting or long-retention system Small teams that want a simple collection surface and will add their own SLO tooling

Infrai exposes one REST API, so a plain HTTP call works from any runtime without installing an SDK; the same collector contract can feed the dashboard while the underlying vendor changes. Its broad, self-describing capability surface also keeps account, storage, and telemetry conventions in one API, which reduces adapter code when the platform roadmap is small. That convenience matters, but it does not remove the need to define retention, alert thresholds, or ownership.

The catch is operational scope. Choose a self-hosted stack when regulatory retention, custom downsampling, or air-gapped operation is non-negotiable. Stick with Datadog when your team needs turnkey cross-signal alerting and accepts hosted metering. Choose AWS-native tools when nearly all spend and traffic live inside AWS. A thin account API is a poor fit if you need a complete incident-management product out of the box.

Verify, then roll back without hiding the signal

Before publishing the dashboard, compare a cached bucket against a direct read over several intervals, record clock skew, and alert on freshness separately from usage. During a key rotation or deploy, keep the old collector available until the new one has produced matching timestamps and totals. If the new path diverges, route readers back to the last known good cache and investigate the delta; do not “fix” it by smoothing away the offending buckets.

Set an SLO for data freshness and a separate SLO for dashboard availability. Those are different promises. A page can be up while its series is stale, and an honest stale marker is safer than a confident zero.

References

Top comments (0)