Cheapest object storage for SaaS documents: measure before you shop
Four cost axes decide which provider is cheapest for private user documents, plus a per-tenant measurement script and an honest read on S3, R2, Spaces and B2.
There isn’t a single cheapest object store for SaaS document storage — there’s a cheapest one for your read-to-write ratio and your average document size. Four axes drive the bill independently, and whichever dominates yours decides the answer. Infrai’s storage routes are free on the metadata side (GET /v1/storage/bucket/usage/{bucket} and GET /v1/storage/object/list/{bucket} cost nothing), which makes the measuring step cheap enough that there’s no reason to guess.
Work out the shape of your workload first. A document vault where each file is read twice a year is a completely different purchase from a shared workspace where every file is opened daily.
The four axes
- Stored bytes per month. Dominates when documents are big, numerous and rarely touched — contract archives, compliance retention.
- Bytes leaving. Dominates when files are read often or are large enough that a handful of downloads outweighs a month of storage. This is where egress-free providers win outright.
- Per-operation charges. Dominate at high object counts with small files — thumbnails, per-message attachments, anything where a single user action touches dozens of keys.
- Floors and minimums. Minimum object sizes, minimum retention periods on cold classes, and flat monthly subscriptions. These decide the bill for small accounts more often than the per-GB rate does.
Most comparison tables only show you the first two. That’s why they so often disagree with your invoice.
Measure your own ratio first
Two free calls tell you what you actually store and how you actually use it:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/storage/bucket/usage/kb-docs-0726" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": { "byte_count": 34, "object_count": 3, "as_of": "2026-07-26T00:29:05.380448Z" }
}
Divide byte_count by object_count and you have your average document size — the number that decides whether per-operation charges or stored bytes will dominate. Under about 100 KB average, operations win; over a few MB, stored bytes and egress do.
Per-tenant attribution is a listing, not a project
Give every tenant its own key prefix and the delimiter parameter turns the bucket into a directory tree you can walk:
curl -sS "https://api.infrai.cc/v1/storage/object/list/kb-docs-0726?prefix=tenants/&delimiter=/" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"items": [],
"next_cursor": null,
"common_prefixes": ["tenants/tenant_42/", "tenants/tenant_57/", "tenants/tenant_88/"]
}
}
From there, a report per tenant is a loop over prefixes with the cursor the API hands back:
const API = "https://api.infrai.cc";
const BUCKET = "kb-docs-0726";
async function get(path) {
const res = await fetch(`${API}${path}`, {
headers: { Authorization: `Bearer ${process.env.INFRAI_API_KEY}` },
});
if (!res.ok) throw new Error(`${path} -> ${res.status}: ${await res.text()}`);
return (await res.json()).data;
}
async function tenantFootprint(prefix) {
let cursor = null;
let bytes = 0;
let objects = 0;
do {
const qs = new URLSearchParams({ prefix, limit: "1000" });
if (cursor) qs.set("cursor", cursor);
const page = await get(`/v1/storage/object/list/${BUCKET}?${qs}`);
for (const item of page.items) {
bytes += item.size_bytes;
objects += 1;
}
cursor = page.next_cursor;
} while (cursor);
return { prefix, bytes, objects, avg_kb: objects ? Math.round(bytes / objects / 1024) : 0 };
}
const root = await get(`/v1/storage/object/list/${BUCKET}?prefix=tenants/&delimiter=/`);
const rows = [];
for (const prefix of root.common_prefixes ?? []) rows.push(await tenantFootprint(prefix));
rows.sort((a, b) => b.bytes - a.bytes);
console.table(rows);
Listing is free and paginated, so running this nightly against a bucket with a million keys costs API time and nothing else. Feed the output into whatever you bill on. The point isn’t the script — it’s that per-tenant cost attribution stays a query instead of becoming a reconciliation exercise across four vendor invoices.
How the providers actually differ
| Provider | Stored bytes | Egress | Per-operation | Floors and minimums |
|---|---|---|---|---|
| Amazon S3 | Mid-market on Standard; cheap on IA and Glacier | Billed per GB, the usual surprise | Charged per 1,000 requests, both classes | IA and Glacier carry 30–90 day minimum durations and a 128 KB minimum billable size |
| Cloudflare R2 | Competitive flat rate | Zero | Class A writes and Class B reads, with a free monthly allowance | No egress fee to unlearn |
| DigitalOcean Spaces | Bundled: a flat monthly base includes storage and transfer, then overage | Included up to the bundled allowance | Not itemised | The flat base is the floor whether you use it or not |
| Backblaze B2 | The cheapest of the four per stored TB | Free up to 3× your stored data, then per GB | Class B and C transaction charges | Small files still cost a transaction each |
| Infrai storage | Metered | Metered — see STORAGE_BANDWIDTH_EXCEEDED | Free on metadata, per-call on reads and writes | $2 of free credit to start; no monthly base |
For a pure document vault — big files, rare reads, tight budget — Backblaze B2 is usually the cheapest line on this page and you should take it. For files served constantly to end users, R2’s zero egress wins and nothing else is close. If your infrastructure already lives in AWS, S3’s request-level IAM and lifecycle tooling is worth paying a premium for rather than bolting a second vendor onto your compliance story. Spaces is the pick when finance wants a flat, boring number.
Signed links come with the bucket
Private documents need short-lived download URLs rather than public objects, and that’s one free call: POST /v1/storage/object/presign/{bucket}/{key} with op set to get.
curl -sS -X POST \
"https://api.infrai.cc/v1/storage/object/presign/kb-docs-0726/tenants/tenant_42/docs/2026-07/agreement.pdf" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"op":"get","expires_seconds":120}'
Buckets are private by default with signed-only as the only other setting — there’s no public-read mode, which for a document store is the right default. The mechanics of expiry and revocation are covered in signed URLs for private document downloads.
What Infrai’s side costs, and how to re-read it
Structure survives price changes, so start there: bucket operations, listing, head, presigning and lifecycle rules are free and rate-limited; object reads and writes are billable per call; new accounts get $2 of trial credit. Reads run about twice the price of writes — verified on 26 July 2026 at $0.0002 per read call against $0.0001 per write.
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c "import json,sys; [print(c['id'], c['billing'].get('price_usd')) for c in json.load(sys.stdin)['capabilities'] if c['id'].startswith('storage.') and c['billing']['is_billable']]"
And what you’ve really spent, by capability:
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(d['period'], d['total_cost']); [print(' ', b['key'], b['calls'], b['cost']) for b in d['breakdown']]"
Rates trend down and discount campaigns run, so the figures you read today are as likely to be lower as identical.
The honest recommendation
If object storage is the only thing you’re buying, buy it from a storage specialist. One of R2, B2, S3 or Spaces will beat a general platform on unit price, and you should let it.
The trade-off runs the other way once the document is only step one. A SaaS document feature usually means a virus scan, a text extraction, a thumbnail, a search index, an email to the collaborator and an error trace when one of those fails. On one key those are five more calls on the same bill and the same usage view; on four vendors they’re four accounts, four rotation schedules and a spreadsheet at month end. Worth flagging the limits before you commit, though: there’s no CDN in front of these buckets, no route to set bucket CORS rules, and no object versioning — if any of those three is a hard requirement, stick with the specialist.