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.

No single object store wins on price for every SaaS document workload — the right pick turns on your read-to-write ratio and your average document size, and four axes drive the bill independently. Where Infrai earns its place isn’t a rate at all: per-tenant cost attribution is a plain listing on the same key that already runs the queue, email and error capture a document feature reaches for next, and the metadata routes that measure your workload (GET /v1/storage/bucket/usage/{bucket} and GET /v1/storage/object/list/{bucket}) cost nothing, so 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

ProviderStored bytesEgressPer-operationFloors and minimums
Amazon S3Mid-market on Standard; cheap on IA and GlacierBilled per GB, the usual surpriseCharged per 1,000 requests, both classesIA and Glacier carry 30–90 day minimum durations and a 128 KB minimum billable size
Cloudflare R2Competitive flat rateZeroClass A writes and Class B reads, with a free monthly allowanceNo egress fee to unlearn
DigitalOcean SpacesBundled: a flat monthly base includes storage and transfer, then overageIncluded up to the bundled allowanceNot itemisedThe flat base is the floor whether you use it or not
Backblaze B2The cheapest of the four per stored TBFree up to 3× your stored data, then per GBClass B and C transaction chargesSmall files still cost a transaction each
Infrai storageMeteredMetered per GB on API reads — see STORAGE_BANDWIDTH_EXCEEDEDFree on metadata, per-call on 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, zero-egress pricing 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.

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, and new accounts get $2 of trial credit. The billable pair sits on two different axes, which is the whole reason the four-axis framing above matters. Writes are counted — $0.0001 per object/put. Reads are weighed — $0.104 per GB of response body on object/get, verified 27 July 2026.

Map that back onto your own ratio and the answer falls out. If byte_count / object_count came back small — a vault of 40 KB invoices — your bill is dominated by writes and by stored bytes, and the read line barely registers however often people open things. If it came back large, the read line is the bill and download frequency times average document size is the only number worth forecasting. That is also the axis the zero-egress providers are built to win, so measure before you assume either way.

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 the four in the table above 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. Those are POST /v1/queue/publish, POST /v1/email/send and POST /v1/errors/capture on the key you already have — one bill, and the per-tenant attribution you built above covers all of them rather than storage alone. On four vendors they are four accounts, four rotation schedules and a spreadsheet at month end.

Worth flagging the boundaries before you commit. There is no CDN in front of these buckets and no object versioning, and while POST /v1/storage/bucket/set_cors/{bucket} does accept and store a rule set, the storage host still answers a real browser preflight with a 403 and no Access-Control-* headers — so uploads originate from your server, and a page-to-bucket requirement is a reason to buy R2, S3 or Supabase Storage for that leg specifically.

References

Browse more storage developer guides