Already on DigitalOcean droplets: Spaces, S3, or neither?

Settle the Spaces-versus-S3 split on egress and colocation rather than per-GB rates, with the honest case for each and where a multi-service API fits instead.

Short version: if your droplets are already on DigitalOcean and what you need is put, get, list and presigned links at moderate volume, Spaces is the default and the half of your team arguing for it is right. Same datacentre, one invoice, an S3-compatible API so a later move is a config change rather than a rewrite. Reach for S3 when you need something only S3 has — fine-grained IAM, lifecycle transitions with day boundaries, cross-region replication, or a compliance document with the word AWS in it.

There’s a third answer that neither half of the team is proposing, and it’s only right in one specific situation: if object storage is one of five services you’re about to bolt on this quarter, something like Infrai puts storage, cron, email and error tracking behind a single credential and a single bill. If storage is genuinely the only thing you need, skip that option — a specialist will serve you better and cheaper.

The argument is about egress, not storage

Per-GB storage rates across the market sit close enough together that they rarely decide anything. Egress is where the order-of-magnitude differences live.

DigitalOcean publishes Spaces at $5/month covering 250 GiB of storage and 1 TiB of outbound transfer, with overage at $0.02/GiB stored and $0.01/GiB transferred. S3 Standard is around $0.023/GiB-month, and internet egress runs $0.09/GiB after the free tier. Read those two transfer numbers next to each other — that’s roughly a 9× gap on the byte that leaves the building, and it’s the whole reason image-heavy and download-heavy apps end up on Spaces, R2 or Backblaze.

So the useful question isn’t “which is cheaper per GB”. It’s “how many GB leave per month, and from where”.

Measure it before you argue about it. If your droplets serve 40 GB of outbound object traffic a month, the difference between these vendors is a rounding error and you should pick on operational fit. If they serve 4 TB, egress is the entire decision.

The three options side by side

DigitalOcean SpacesAmazon S3Infrai storage
Colocated with DO dropletsyes, same regionno, unless you move computeno — see the region caveat below
Billing shapeflat monthly base plus overagepure per-GB and per-requestper-call, plus metered GB-month and egress
Egress cost profilea bundled TiB, then a low per-GiB rateper-GiB after the free tier, an order of magnitude highermetered, read from the usage API
Lifecycle: delete after N daysyesyesyes (expire_days)
Lifecycle: transition after N dayslimitedyes (Transition.Days)not supported
CDN in frontbuilt inCloudFront, separatenot included
Other services on the same keythe rest of DigitalOceanthe rest of AWSAI, email, cron, queues, metrics
S3-compatible APIyesit is the APIREST, not S3 wire-compatible

When S3 earns the second invoice

Be honest with the team about what actually justifies leaving the DO bill:

You need IAM policies at object-prefix granularity, per-role, auditable. You need objects to move to a colder class on a day boundary — Spaces has cold storage, but if the schedule matters, S3’s Transition.Days is the thing that expresses it. You need cross-region replication as a contractual durability story. Or the rest of your data platform — Athena, Glue, Lambda triggers, SageMaker — already lives in AWS and the object store is just the front door.

None of those are “S3 is faster” or “S3 is cheaper”, because for this workload it usually isn’t either.

The colocation caveat, stated plainly

If the reason you’re leaning toward Spaces is that the bytes stay inside DigitalOcean’s network, that’s a real advantage and Infrai can’t match it. Region on Infrai is not a placement guarantee. We created a bucket asking for eu-central-1, and the API reported that region back happily:

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/storage/bucket/create" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"name":"kb-do-eu-0726","region":"eu-central-1","acl":"private"}'
{
  "ok": true,
  "data": {
    "bucket_id": "bkt_92f376647d874cac9998eb",
    "name": "kb-do-eu-0726",
    "vendor": "cos",
    "region": "eu-central-1",
    "acl": "private",
    "cors_rules": [],
    "lifecycle_rules": []
  }
}

Then we asked for a presigned URL to an object in it, and the host that came back was cos.ap-singapore.myqcloud.com. The field is metadata, not placement. If low latency from your Frankfurt droplets or a data-residency commitment is on the table, that’s a hard limitation and Spaces or S3 is the correct pick — don’t let a field name carry a promise it isn’t making.

Working the Infrai side

Buckets, objects and usage are plain REST. Nothing here needs an SDK:

curl -sS "https://api.infrai.cc/v1/storage/bucket/usage/kb-spaces-0726" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "byte_count": 12288,
    "object_count": 3,
    "as_of": "2026-07-26T00:55:33.964369Z"
  }
}

And the estimate your finance person actually wants — total bytes across every bucket on the account, which is the input to any GB-month projection:

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");

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;
}

const { items } = await readJson("/v1/storage/bucket/list");
let totalBytes = 0;
let totalObjects = 0;

for (const bucket of items) {
  const usage = await readJson(`/v1/storage/bucket/usage/${bucket.name}`);
  totalBytes += usage.byte_count;
  totalObjects += usage.object_count;
  console.log(
    `${bucket.name.padEnd(28)} ${(usage.byte_count / 1e9).toFixed(3)} GB  ${usage.object_count} objects`,
  );
}

console.log(`\ntotal: ${(totalBytes / 1e9).toFixed(3)} GB across ${items.length} buckets, ${totalObjects} objects`);

Run that against a week of real traffic and the Spaces-versus-S3 argument usually settles itself, because one side of the room discovers the monthly delta is smaller than the meeting they held about it.

What the calls cost, and how to check

Storage writes are $0.0001 per call and reads $0.0002; bucket creation, listing, usage and presigning are free and rate-limited. Verified 26 July 2026, with $2 of credit on a new account. Stored bytes and egress are metered separately from the call charges, so read your own spend rather than modelling it:

curl -sS "https://api.infrai.cc/v1/account/usage" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "period": "30d",
    "total_cost": 4.21,
    "total_calls": 8172,
    "breakdown": [
      { "key": "storage.object.put", "label": "storage.object.put", "cost": 0.4229, "calls": 4229 },
      { "key": "storage.object.get", "label": "storage.object.get", "cost": 0.0266, "calls": 11842 }
    ]
  }
}

Rates across this market keep drifting downward and vendors run campaigns, so whatever you read today is likely at or below these figures. The durable point isn’t the rate — it’s that the per-tenant attribution above is a single GET rather than a spreadsheet reconciling three vendors.

How we’d settle the split

Stay on Spaces if the workload is images, backups and user downloads served from DO compute. It’s the boring answer and boring is correct here.

Move that one bucket to S3 if a specific S3 feature is load-bearing — day-boundary tiering, prefix-level IAM, replication — and accept the second invoice for it. That’s a per-bucket decision, not a platform migration, and framing it that way usually ends the argument.

Consider Infrai only when the next three tickets after “add object storage” are “add a nightly job”, “send a notification email” and “capture errors”, because that’s the case where one credential and one bill genuinely beats four accounts. If you need colocation with your droplets or an S3-wire-compatible client library, it isn’t the right tool and Spaces remains the answer.

References

Browse more storage developer guides