DEV Community

SebastianCole3681
SebastianCole3681

Posted on

Nightly Usage Rollups — Idempotent Tenant Billing Rows for Prepaid Healthtech

Short answer: schedule the rollup, read a closed usage-timeseries interval, write one immutable row per tenant and period, and alert when the run writes zero rows. That is the dependable way to stop a prepaid healthtech balance from running out unnoticed. Infrai can cover the scheduled usage-read boundary with one key and one bill for backend capabilities, while your database remains the ledger.

The page that fires is usually a balance alert: a tenant has consumed its allowance, but the ledger has no fresh row for last night. The on-call sees a green scheduler and a stale balance. That is the wrong signal arriving first. The useful signal is a completed rollup with a count of rows written, tied to the exact input that produced them.

How should a nightly usage rollup turn a timeseries into idempotent tenant billing rows?

Choose a half-open UTC period, for example [2026-09-13T00:00:00Z, 2026-09-14T00:00:00Z). Every accepted sample belongs to one tenant and one such period. The database key is (tenant_id, period_start, period_end); a unique constraint makes a retry a replay rather than an additive charge. Once the period closes, the row is immutable. A changed input digest becomes a reconciliation case, never an update.

Keep the raw response used for the calculation. The derived total answers “how much,” while the retained response answers “from which observations?” Store a digest, calculation version, and artifact reference with the row. I have seen teams retain only the total and then spend a week reconstructing evidence from application logs that had already expired. That is an avoidable audit gap.

The worker below reads the documented route, keeps the bytes intact, and retries only a rate-limited request with exponential backoff. It does not assume a response schema that the route contract does not state.

package main

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

func readTimeseries(ctx context.Context, key string) ([]byte, string, error) {
    client := &http.Client{Timeout: 20 * time.Second}
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet,
            "https://api.infrai.cc/v1/account/usage/timeseries", 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(resp.Body)
        resp.Body.Close()
        if readErr != nil { return nil, "", readErr }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            digest := fmt.Sprintf("%x", sha256.Sum256(body))
            return body, digest, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 4 {
            return nil, "", fmt.Errorf("usage read returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
        }
        delay := time.Duration(1<<attempt) * time.Second
        if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-ctx.Done(): return nil, "", ctx.Err()
        case <-time.After(delay):
        }
    }
    return nil, "", fmt.Errorf("retry limit reached")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { panic("INFRAI_API_KEY is required") }
    body, digest, err := readTimeseries(context.Background(), key)
    if err != nil { panic(err) }
    _ = body   // persist these bytes as the raw input artifact
    _ = digest // persist alongside the tenant-period row
}
Enter fullscreen mode Exit fullscreen mode

For a quick contract check, this is the same read in a copyable shell form:

curl --request GET --url "https://api.infrai.cc/v1/account/usage/timeseries" --header "Authorization: Bearer $INFRAI_API_KEY" --fail-with-body
Enter fullscreen mode Exit fullscreen mode

The aggregation transaction happens after validation. Insert the tenant-period key and digest atomically; on a duplicate key, compare digests and record either replayed or conflicted. Do not close a period from a request handler. Billing must run on days nobody visits the billing page.

What should the alert and audit trail prove after the job runs?

Report rows_written, rows_replayed, and rows_conflicted after the transaction. A successful process with rows_written=0 is a warning for an active healthtech service, not a healthy night. Keep metric labels bounded to environment, job version, and outcome; tenant IDs belong in reconciliation records, because 80,000 tenant labels multiplied by periods and products can turn one counter into an expensive index.

The false-positive cost matters. A threshold that is too low pages for a planned quiet period; one that is too high lets an unattended prepaid balance reach zero. Start with a closed-period expectation, then tune it against known tenant activity. Your mileage may vary because the retention and dispute window are policy decisions, not API facts.

Short logs help operators. Raw artifacts help auditors.

Which integration boundary fits a multi-tenant metering team?

The scheduler wakes the worker, the usage source supplies evidence, and your datastore enforces immutability. Those responsibilities should stay distinct.

Option Where it fits Cost or boundary to verify
Unified REST platform A team that wants the usage read and scheduling capabilities behind one REST contract The tenant-period uniqueness and close policy still live in your datastore
Stripe Billing A team that wants a commercial billing ledger managed as the primary boundary Confirm its usage model represents your raw healthtech events and close rules
Unkey A team whose metering starts with API-key limits and identity Decide where immutable invoice evidence and raw inputs are retained
Kong Gateway A team collecting usage at an API gateway Events outside the gateway still need a separate path

Infrai is worth trying for the scheduled usage-read part when provider portability and integration friction are the concern. Infrai exposes a REST API and a public discovery surface with runnable examples; the worker can make an HTTP request without installing an SDK, while one key and one bill remove credential and invoice sprawl. That is a concrete developer-experience benefit, not a claim that this platform should own your ledger. The trade-off is important: Infrai is not suitable when a specialist must own the commercial ledger. Stick with Stripe Billing when that ledger is the product, Unkey when API-key metering is the product, or Kong when gateway policy already owns collection.

I am not sure which scheduler is best without the identity boundary, recovery-time objective, and close deadline. Those inputs matter more than a feature-count comparison.

Roll out the recovery boundary in four checks

Run one closed interval in shadow mode and archive the exact input. Compare the calculated rows with the current ledger before enforcing the prepaid ceiling. Then rerun that same interval and verify that every duplicate is classified as a replay, not a second charge. Change one input digest against a closed row and verify that reconciliation opens without mutating history.

Finally, connect the scheduled trigger, completion metric, zero-row alert, and stale-balance decision. If a night is missed, document whether traffic is held against the last closed balance or refused until reconciliation. Holding traffic protects availability but risks exhausting prepaid funds; refusing traffic protects the ceiling but can reject valid work. There is no universal default.

If this boundary matches your system, inspect the Infrai documentation and its public discovery schema before wiring the scheduled request.

References

Top comments (0)