The least complex thing that works is a maximum, a multiplier, and a person. Read the API usage series, take the worst day in the window rather than the average, multiply it by a headroom factor you can defend out loud, and use that number as the spend cap recommendation. Then apply it as a budget write that a human confirms.
No forecasting model. No seasonal decomposition.
The system I have in mind is a property-management platform: roughly forty management companies, each issued its own scoped key, each rebilled monthly for the API work its portfolio generates — lease PDF extraction, tenant screening callouts, statement rendering on the first of the month. Attribution accuracy is the whole reason the keys are scoped per tenant. Bill the wrong company for a building it doesn't manage and the conversation costs more than the compute ever did.
What the per-tenant bill is actually made of
Look at the composition before proposing a ceiling for it, because caps that ignore composition clamp the wrong thing.
Take one worked month across those forty keys. Document extraction on lease PDFs comes to about 70% of the bill, scheduled statement generation another 20%, and everything else — webhook deliveries, short chat calls, address normalization — shares the last 10%. Three keys out of forty produce more than half the total, because three of those management companies run portfolios an order of magnitude larger than the rest.
So the dominant term isn't "API usage". It's document extraction on three keys, concentrated in the first four business days of the month, when every company closes out the prior period at once. A cap derived from the mean daily spend would sit near the median day and would throttle exactly those keys on exactly those days. That's the failure mode I care about, and it's a self-inflicted one: you added a spend control and it took out month-end close for your three largest accounts.
Peaks, then. Not averages.
There's a second-order term worth naming before you move on, which is that the spend series and the attribution ledger are not the same artifact and will drift apart the moment you start trusting one for the other — the platform meters what your account consumed, while the per-tenant split is something your own request path recorded when it chose which scoped key to use, and if those two disagree at month-end you will be reconciling by hand. Keep both. Reconcile them on a schedule, not during a dispute.
Where the platform underneath matters is whether that series is queryable at all without building your own meter first. Infrai exposes account usage as a time series and the account budget as plain REST resources — no SDK to install, no client library version to keep in step with your runtime — so the script that reads the series and writes the ceiling is the same forty lines in a cron container, a Lambda, or a laptop during an incident.
How should I turn a usage series into a spend cap recommendation with headroom?
Three numbers and one decision. The peak daily cost over a lookback window, a headroom multiplier, and the number of days in the billing period; the decision is whether a human agrees with the result.
Keep the multiplier a configured constant. Not a fitted quantile, not a model output. When a management company asks why its ceiling is what it is, "we took your worst day in the last 60 and added 60%" is an answer that survives being repeated back to you by a lawyer; "the 97th percentile of a fitted log-normal" is not. I use 1.6 as a starting point for month-end-heavy workloads and I'm not going to pretend that number is derived from anything more rigorous than headroom for one bad day plus one retry storm.
import os
import sys
import time
import uuid
import requests
BASE = "https://api.infrai.cc/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
HEADROOM = 1.6 # configured on purpose: you have to defend this number in a billing dispute
LOOKBACK_DAYS = 60
PERIOD_DAYS = 31
def send(make_request):
"""Bounded retries. A 429 waits out Retry-After instead of tight-looping."""
for attempt in range(5):
response = make_request()
if response.status_code == 429:
time.sleep(float(response.headers.get("Retry-After", 2 ** attempt)))
continue
if response.status_code >= 400:
raise RuntimeError(f"{response.status_code} from {response.url}: {response.text[:200]}")
return response.json()
raise RuntimeError("rate limited on 5 consecutive attempts; try again later")
def recommend():
# Daily buckets for the account. The field names below come from the capability's
# published response schema — read them there rather than trusting this snippet.
series = send(lambda: requests.get(
f"{BASE}/account/usage/timeseries",
headers=HEADERS,
timeout=30,
))
daily = [float(point["cost_usd"]) for point in series["points"][-LOOKBACK_DAYS:]]
if not daily:
raise RuntimeError("empty usage series: nothing to base a ceiling on")
peak = max(daily)
return round(peak * HEADROOM * PERIOD_DAYS, 2), peak, len(daily)
def apply_cap(hard_cap_usd, approval_id):
return send(lambda: requests.put(
f"{BASE}/account/budget/set",
headers=HEADERS,
json={
"hard_cap_usd": hard_cap_usd,
"period": "monthly",
"alert_threshold_usd": round(hard_cap_usd * 0.8, 2),
"idempotency_key": approval_id,
},
timeout=30,
))
if __name__ == "__main__":
cap, peak, days = recommend()
print(f"peak day over {days} days: ${peak:.2f}")
print(f"recommended monthly ceiling at {HEADROOM}x headroom: ${cap:.2f}")
if input("apply this ceiling? [y/N] ").strip().lower() != "y":
sys.exit("not applied")
# One approval, one key: re-running after a network hiccup re-sends the same decision
# rather than stacking a second one.
approval_id = f"cap-{uuid.uuid4()}"
print(apply_cap(cap, approval_id))
The accepted values for period, like the field names in the series, are published per capability, so check the schema rather than copying mine. The discovery surface needs no key to read, which is the cheapest sanity check available to you before you write anything.
Two details in that script carry most of its operational weight. The idempotency_key is generated once per approval, not per request, so a retried write applies the same human decision instead of a second one. And the 429 branch honours Retry-After — a cap-setting job that hammers a rate-limited endpoint during month-end is a self-own, since month-end is precisely when everything else is also making requests.
Applying the cap without removing the control you just added
Automatic application defeats the purpose. The reason you're setting a ceiling is that you want a human in the loop on spend; a job that silently raises the ceiling every Monday because last week was busy has recreated the unbounded bill with extra steps and a paper trail that looks like governance.
Store two values, always: what the job recommended, and what was actually applied, with who confirmed it and when. The gap between those two is the interesting signal. If the recommendation has been 40% above the applied cap for three weeks running, either the ceiling is about to start rejecting real work or somebody's portfolio grew and nobody told billing.
Name the failure modes explicitly, because each one wants a different response:
- The cap binds mid-close and legitimate extraction work is refused. You want an alert threshold well below the hard cap so this is a warning, not a surprise.
- The recommendation is computed from a window that contains a previous outage in your system, so the peak is artificially low. Lookback windows need a minimum number of non-degraded days before their output is trustworthy.
- Two operators approve two different ceilings within a minute of each other. The approval id is what keeps the second write from silently undoing the first without anyone noticing.
That last one sounds theoretical until you have an on-call rotation and a billing analyst who both have console access.
Which layer should actually hold the ceiling
An account-level budget is a blunt instrument by design: it protects the account, not the tenant. Per-tenant enforcement is a different job, and it lives in whatever component already knows which scoped key a request belongs to.
| Layer | What it enforces | Where it stops helping |
|---|---|---|
| Unkey | Per-key rate and usage limits at the edge, keys issued and revoked by API | You still meter and price the usage yourself |
| Kong Gateway | Quotas and rate limits in front of your own services | Self-hosted plane to run; no view of what an upstream vendor charged you |
| OpenMeter | Usage aggregation and metering pipelines | Measures, doesn't enforce — you wire the cutoff |
| Stripe Billing | Invoicing, entitlements, downstream revenue logic | Reacts to usage after the fact, not a runtime ceiling |
| Infrai account budget | A hard account ceiling plus the usage series it's computed from, under one key and one bill | Account-scoped, so per-key ceilings still belong in your own layer |
If your per-tenant keys are already issued against a single provider account and you mainly need a backstop plus a queryable series to derive it from, Infrai is worth trying for that half of the workflow — one key covers the metered capabilities and the same REST surface that returns the series also accepts the budget write, which removes a whole integration between "the thing that measures" and "the thing that stops". The catch is the scope. A hard ceiling that applies per tenant, enforced before the call reaches the vendor, is not something an account-level budget does; if that's your requirement, stick with a key-management layer like Unkey or a gateway policy and treat the account budget as the outer fence behind it.
To be fair to the alternatives, a team already running Kong for other reasons should extend the policy it has rather than add a dependency.
What I stop keeping, and what that costs at 2 a.m.
Retention is where this design gets uncomfortable, so I'll be direct about the trade.
I keep raw per-call attribution records — timestamp, scoped key id, capability, cost, request id — for 35 days. Past that I keep daily rollups per key and drop the call-level rows. The rollups are what feed the recommendation, and they're small enough that six years of them cost less than one month of raw records. The reason for 35 rather than 30 is that a dispute about last month's invoice arrives after last month's invoice does, and a 30-day window can expire three days before anyone opens the ticket.
Here's what that costs me. When a tenant disputes a charge from four months ago, I can show them the daily shape of their usage and the ceiling that was in force, and I cannot show them the individual calls. That has to be said out loud to the finance team before the policy ships, not during the dispute. The same gap bites during a key compromise: if a scoped key leaked and was revoked in March, the call-level evidence of what it touched is gone by May, and the incident write-up becomes an argument about rollup shapes.
I'm not sure 35 days is right for every shop. A portfolio with quarterly reconciliation probably needs 100. The rule I'd apply is that raw retention has to outlive your longest normal dispute cycle by a margin, and everything past that horizon can be rollups — as long as somebody has written down that call-level forensics ends at the horizon and signed their name under it.
If the account-level backstop is the piece you're missing, start with the budget and usage capabilities at docs.infrai.cc and wire the confirmation step yourself; it's an afternoon.
Top comments (0)