Cheapest object storage for app backups: price the restore, not the archive

Backups make storage look cheap and restores make it expensive. A cost model, the retention floors nobody mentions, and where Infrai fits against B2, R2 and S3.

Backup storage is the one workload where the sticker price misleads almost everyone. You write a few hundred gigabytes a month, read approximately none of it, and then one bad Tuesday you pull the whole archive back down at once. That single day is where the money is, and it’s the line no per-GB comparison shows you. Infrai’s storage routes are worth a look here because the metering calls are free — you can measure your real archive before choosing anything.

Below is the model we’d use to decide, the retention rules that make expiry the bucket’s job rather than a script’s, and an honest account of where a specialist backup vendor wins.

Four lines on the bill, two that matter for backups

Stored bytes per month, bytes leaving, operations, and floors. Backups barely touch operations — a nightly dump is one object — so the fight is between the first two, mediated by the fourth.

Work out your own numbers before reading anyone’s pricing page. If you keep 30 daily dumps of 12 GB, you’re storing 360 GB steadily, adding 12 GB and dropping 12 GB each night. At a hypothetical $6 per TB-month that’s about $2.16 a month to hold. A single full restore of 360 GB at $0.09 per GB of egress is $32.40 — fifteen months of storage, spent in one afternoon. That ratio, not the per-GB rate, is what should decide the purchase.

Retention floors are the charge nobody quotes

Cold storage classes almost always come with a minimum retention period. Delete an object before the floor and you’re billed as if you’d kept it. For a rotating 30-day backup set against a 90-day floor, you pay for every object three times over, which quietly triples a bill that looked like the cheapest option on the comparison page.

Check three things on any vendor’s page before you commit: minimum retention days, minimum billable object size, and whether egress is free, capped, or metered per GB.

PropertyWhat to look forWhy backups care
Egress pricingfree, capped at a multiple of storage, or per GBa full restore is one huge read
Minimum retention0, 30, or 90 daysrotating sets get billed for deleted objects
Minimum object size0 or 128 KBirrelevant for dumps, brutal for per-file archives
Object Lock / WORMavailable or notransomware and insider-deletion protection
Region choiceproven by the endpointUS/EU contractual commitments

Backblaze B2 publishes free egress up to a multiple of what you store, which is the friendliest structure for restore-heavy workloads. Cloudflare R2 charges nothing for egress at all. Amazon S3 charges per gigabyte out and always has, which is fine if you never restore and expensive on the day you do.

Where Infrai fits — and where it doesn’t

The storage surface here is S3-compatible underneath, private-only, with per-prefix retention rules and free metering. It’s a good fit when the backup job is one of several things your app already does through the same key: the cron that triggers the dump, the queue that runs it, the email that reports failure, and the error capture when it dies are all on that credential.

It’s a poor fit for two specific requirements. There’s no Object Lock or WORM mode, so if your threat model includes an attacker with your API key deleting the backups, you need a vendor that offers immutability — Backblaze B2 or S3 with compliance-mode lock. And the region field is recorded but not proven: a bucket we created as eu-central-1 on 2026-07-26 presigned to an ap-singapore host. For a contractual EU-residency commitment, stick with a provider whose endpoint hostname is the guarantee.

Retention as a bucket rule

One call, and the storage does the expiring. Send the complete rule set every time — this replaces rather than merges:

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/storage/bucket/set_lifecycle/kb-appbackup-0726" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"rules": [{"prefix": "backups/db/", "expire_days": 30}, {"prefix": "backups/weekly/", "expire_days": 365}]}'

Daily dumps live a month, the Sunday copy lives a year. A malformed rule comes back as STORAGE_INVALID_LIFECYCLE_RULES instead of being accepted and ignored, which matters when the rule is the whole retention control.

Uploading the dump

DUMP_B64=$(base64 < app-postgres.sql.gz | tr -d '\n')
printf '{"data_base64":"%s","content_type":"application/gzip"}' "$DUMP_B64" > dump.json

curl -sS -X PUT \
  "https://api.infrai.cc/v1/storage/object/put/kb-appbackup-0726/backups/db/2026-07-26/app-postgres.sql.gz" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary @dump.json

Base64 inflates the payload by a third, so anything past about 10 MB belongs on the multipart routes instead. For a compressed dump that threshold arrives quickly.

What the archive actually holds

Two free calls answer “how much am I storing” and “which restore points exist”:

curl -sS "https://api.infrai.cc/v1/storage/bucket/usage/kb-appbackup-0726" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

curl -sS "https://api.infrai.cc/v1/storage/object/list/kb-appbackup-0726?prefix=backups/db/" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "byte_count": 21,
    "object_count": 1,
    "as_of": "2026-07-26T00:57:37.999725Z"
  }
}

Feed byte_count into your own cost model monthly. Because the call is free, there’s no reason not to graph it — and a backup archive that stops growing is usually a backup job that stopped running.

The restore path

A restore is a download of one large object, and a signed URL is the practical way to get it onto an operator’s machine without proxying gigabytes through your app.

curl -sS -X POST \
  "https://api.infrai.cc/v1/storage/object/presign/kb-appbackup-0726/backups/db/2026-07-26/app-postgres.sql.gz" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"op": "get", "expires_seconds": 900}'

Fifteen minutes is plenty; expires_seconds accepts 1 to 604800 if you need longer. Do the operator authorization and the audit-log write in your own code before you call this — the URL itself is a bearer credential, not a permission check, and it shouldn’t be the thing standing between an intern and last night’s customer database.

Then confirm what you’re about to restore is really there:

curl -sS \
  "https://api.infrai.cc/v1/storage/object/head/kb-appbackup-0726/backups/db/2026-07-26/app-postgres.sql.gz" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

found is a field in a 200 response rather than an HTTP status, so check the body. An untested restore isn’t a backup, and the cheapest storage in the world doesn’t help if the dump is truncated.

Reading Infrai’s own numbers

Buckets, lifecycle, listing, usage, head and presign are free and rate-limited. Object writes bill $0.0001 per call — verified 2026-07-26 against real metered usage, not the rate card — so a nightly dump costs a fraction of a cent per year in call charges, and the stored bytes are metered separately. New accounts start with $2 of credit.

curl -sS "https://api.infrai.cc/v1/account/usage" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | jq '.data.breakdown[] | select(.key | startswith("storage."))'

Storage rates trend downward and discounts run, so treat that output as authoritative and this page as an illustration. The durable claim isn’t the number: it’s that one nightly job needs a scheduler, a bucket, a retention rule, an alert and somewhere to record the failure — and those come off one key and land on one invoice instead of four.

If backups are all you’re buying, and immutability or proven regions are on your requirements list, buy Backblaze B2 and don’t think twice. The consolidation argument only holds when the backup is one job among many.

References

Browse more storage developer guides