Watching per-bucket storage growth before the invoice arrives
Sample bucket usage on a schedule, alert on the slope rather than the size, and put a hard spend cap behind it — three free Infrai calls and about 40 lines of Node.
The number that saves you isn’t how big a bucket is — it’s how fast it’s getting bigger. A bucket that grew 40 MB last week and 4 GB this week has already told you the invoice story three weeks early, and a dashboard showing only the current total won’t. Infrai gives you the raw material in three calls that are all free and rate-limited: per-bucket usage, an account-wide daily cost series, and a budget cap that stops the bleeding on its own.
None of it is automatic. There’s no built-in usage alarm, so the loop below is something you run on a schedule — that’s the honest shape of the answer, and roughly 40 lines of Node.
The three reads
Per-bucket size and object count, as a point-in-time sample:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/storage/bucket/usage/kb-ai-gallery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"byte_count": 262144,
"object_count": 4,
"as_of": "2026-07-27T12:07:41.739662Z"
}
}
Note as_of. That’s a measurement timestamp, not a period — this endpoint has no history and no aggregation. Whatever series you want to plot, you build by sampling it.
Account-wide daily spend, which does have history:
curl -sS "https://api.infrai.cc/v1/account/usage/timeseries?granularity=day" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"period": "30d",
"buckets": [
{ "date": "2026-07-05", "cost": 0.06477577, "calls": 131, "failed_calls": 0 },
{ "date": "2026-07-06", "cost": 1.18094988, "calls": 1003, "failed_calls": 0 },
{ "date": "2026-07-07", "cost": 0.2753909, "calls": 5749, "failed_calls": 0 }
],
"next_cursor": null
}
}
And the current budget state, which tells you whether anything is guarding you at all:
curl -sS "https://api.infrai.cc/v1/account/budget/get" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"hard_cap_usd": null,
"period": "monthly",
"alert_threshold_usd": null,
"spent_this_period_usd": 12.61464367,
"configured": false,
"updated_at": null
}
}
"configured": false is the state most accounts are in, and it means nothing will stop a runaway job.
Sample the slope, not the total
Here’s the watcher. It walks every bucket on the account, records a sample, compares against the previous run, and fails loudly when a bucket’s growth rate would put it over a per-bucket ceiling before the month ends.
import { readFile, writeFile } from "node:fs/promises";
import process from "node:process";
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const HISTORY = "./bucket-usage-history.json";
// Per-bucket ceiling in gigabytes; anything not listed uses the default.
const CEILINGS = { "kb-ai-gallery": 5, default: 50 };
async function readJson(path) {
const res = await fetch(`${API}${path}`, {
method: "GET",
headers: { Authorization: `Bearer ${KEY}` },
});
const json = await res.json();
if (!res.ok || json.ok === false) {
const e = json.error ?? {};
throw new Error(`GET ${path} -> HTTP ${res.status} ${e.code ?? ""} ${e.message ?? ""}`);
}
return json.data;
}
async function loadHistory() {
try {
return JSON.parse(await readFile(HISTORY, "utf8"));
} catch (err) {
if (err.code === "ENOENT") return {};
throw err;
}
}
const history = await loadHistory();
const { items } = await readJson("/v1/storage/bucket/list");
const now = Date.now();
const breaches = [];
for (const bucket of items) {
const usage = await readJson(`/v1/storage/bucket/usage/${bucket.name}`);
const gb = usage.byte_count / 1e9;
const previous = history[bucket.name];
const ceiling = CEILINGS[bucket.name] ?? CEILINGS.default;
let projected = gb;
if (previous) {
const days = (now - previous.at) / 86_400_000;
if (days > 0.5) {
const perDay = (gb - previous.gb) / days;
projected = gb + perDay * 30;
console.log(
`${bucket.name.padEnd(26)} ${gb.toFixed(3)} GB ${perDay >= 0 ? "+" : ""}${perDay.toFixed(3)} GB/day →30d ${projected.toFixed(2)} GB`,
);
}
} else {
console.log(`${bucket.name.padEnd(26)} ${gb.toFixed(3)} GB (first sample)`);
}
if (projected > ceiling) {
breaches.push(`${bucket.name}: projected ${projected.toFixed(2)} GB in 30 days, ceiling ${ceiling} GB`);
}
history[bucket.name] = { gb, objects: usage.object_count, at: now };
}
await writeFile(HISTORY, JSON.stringify(history, null, 2));
if (breaches.length) {
console.error(`\nprojection breaches:\n ${breaches.join("\n ")}`);
process.exitCode = 1;
}
Run it hourly and the first useful signal arrives within a day. Run it daily and you’ll catch a leak inside a week — comfortably ahead of the invoice.
The exit code is the integration point. Wire it to whatever already pages you, or keep the whole thing on one credential: POST /v1/cron/create runs the script on a schedule, POST /v1/email/send delivers the breach list, and POST /v1/errors/capture records the run that threw. No second account, no monitoring vendor to onboard, and the cost of the alerting shows up on the same bill as the storage it’s watching.
Put a floor under it
Detection is not protection. A cap is:
curl -sS -X PUT "https://api.infrai.cc/v1/account/budget/set" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"hard_cap_usd":200,"period":"monthly","alert_threshold_usd":150}'
hard_cap_usd blocks spend past the cap for the period and alert_threshold_usd fires before you get there. It’s account-wide rather than per-bucket, which is a real limitation — you can’t cap one tenant’s bucket independently — but it converts “we found out at month end” into “the job stopped on the 19th”, which is the failure mode you actually wanted.
What to watch, and what a bad reading looks like
| Signal | Where it comes from | Reacts within | Bad looks like |
|---|---|---|---|
| Bucket bytes | GET /v1/storage/bucket/usage/{bucket} | your sampling interval | slope changes by more than 2× week over week |
| Object count | same call | your sampling interval | count climbing while bytes are flat — a temp-file leak |
| Daily API spend | GET /v1/account/usage/timeseries | next day | one capability going from cents to dollars |
| Spend per capability | GET /v1/account/usage | next day | storage.object.get outrunning storage.object.put |
| Remaining runway | GET /v1/account/balance | continuous | runway_days under your top-up cycle |
| Period spend vs cap | GET /v1/account/budget/get | continuous | configured: false |
GET /v1/account/balance is the underrated one — it returns daily_avg_spend and runway_days already computed, plus an affordable_uses_hint block that converts your balance into remaining calls per capability. One read answers “when do we run out” without you modelling anything.
Two blind spots and one asymmetry
A multipart upload that was started and never completed isn’t an object yet, so its parts don’t appear in byte_count or object_count. Track your open upload_id values and call DELETE /v1/storage/multipart/abort/{upload_id} on the stale ones — a sweeper, not a graph.
Egress isn’t in bucket usage at all. That’s the asymmetry that catches people out, because the two meters are shaped differently: writes bill per call, and reads bill by the gigabyte. A bucket holding one hot public asset and a bucket holding a cold archive of the same size look identical in byte_count and can differ by two orders of magnitude on the invoice. Watch storage.object.get in GET /v1/account/usage alongside the size graph, and if the answer is “lots of reads of the same objects”, the fix is a CDN in front rather than a smaller bucket.
And there’s no server-side threshold alert on bucket size. POST /v1/storage/bucket/set_notification/{bucket} subscribes a callback to object events — object.created, object.deleted, multipart.completed — which reacts to individual writes but says nothing about aggregate size. If you want a managed alarm with no polling loop of your own, that’s a genuine gap: S3 with CloudWatch metrics and Storage Lens, or GCS with Cloud Monitoring, gives you that out of the box, and if a monitoring SLA is the requirement you’d be better off buying it there.
What the watching costs
Every call in this article is free and rate-limited: bucket listing, bucket usage, account usage, the timeseries and the budget reads all bill nothing and don’t touch the $2 of credit a new account starts with. Only the object operations are billable — PUT /v1/storage/object/put/{bucket}/{key} at $0.0001 per call, and GET /v1/storage/object/get/{bucket}/{key} metered by egress volume at $0.104 per GB, both read on 27 July 2026. Check today’s figures and your own spend from the same place:
curl -sS "https://api.infrai.cc/v1/discovery?namespace=storage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Rates in this market trend downward and campaigns run, so what you read is likely at or below those figures. That the monitoring is free matters more than the rate — a polling loop you’d hesitate to run hourly is a polling loop that won’t catch anything.
The structural advantage is quieter than any number: storage, the cron that samples it, the mail that reports it and the queue behind your workers all meter into one usage view and one bill, so “what did this tenant cost us” is a prefix sum from the loop above plus a single spend query — not a reconciliation across four vendors with four billing periods and four exports.