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.
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 for that 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 here, so the loop below is something you run on a schedule — which is 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-usage-0726" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"byte_count": 262144,
"object_count": 4,
"as_of": "2026-07-26T00:59:04.459998Z"
}
}
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-22", "cost": 0.03777135, "calls": 733, "failed_calls": 0 },
{ "date": "2026-07-23", "cost": 1.74311769, "calls": 2237, "failed_calls": 0 },
{ "date": "2026-07-24", "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": 9.756823,
"configured": false
}
}
"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 whole 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-usage-0726": 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 — still comfortably ahead of the invoice.
The exit code is the integration point. Wire it to whatever already pages you, or, since the scheduler and the notification channel are on the same Infrai key, make it a cron job that sends a mail on failure and skip the separate monitoring vendor entirely.
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 '{"limit_usd":200,"period":"monthly","alert_threshold_pct":75}'
That blocks spend past the cap for the period and fires an alert at 75% of it. 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, so a single read answers “when do we run out” without you modelling anything.
Three blind spots, stated up front
Multipart uploads that never completed don’t appear in byte_count. We opened an upload, wrote an 8 MiB part and left it; the bucket still reported zero bytes and zero objects. So this measurement under-reports whatever the vendor is really holding, and abandoned uploads need their own sweeper rather than showing up on this graph.
Egress isn’t in bucket usage at all. Bytes served out are metered separately and surface through account spend, not per-bucket counters, so a bucket with a hot public asset looks identical to a cold archive of the same size.
And there’s no server-side threshold alert. POST /v1/storage/bucket/set_notification/{bucket} subscribes a callback to object events — object.created, object.deleted, multipart.completed — which is useful for reacting to individual writes but doesn’t help with aggregate size. If you want a managed alarm with no polling loop of your own, that’s a genuine gap, and S3 with CloudWatch metrics and Storage Lens, or GCS with Cloud Monitoring, will give you it out of the box.
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 reads and writes themselves are billable, at $0.0002 and $0.0001 per call — verified 26 July 2026. Check today’s figures:
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Storage rates in this market trend downward and campaigns run, so what you read is likely at or below those numbers. 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 trade-off worth naming: you’re assembling this yourself. If continuous storage monitoring is a hard requirement with an SLA behind it and you don’t want to own a cron job and a history file, use a platform where it’s a managed product. If what you actually need is to not be surprised, an hourly script and a hard cap will do it for a few pennies a month in nothing at all.