Browser-direct uploads: what presigned URLs actually cost on Infrai

Presigning is free on Infrai and you pay only for the write. How the cost actually breaks down against R2, S3 and Supabase Storage — and where each one still wins.

If you want a browser to upload straight to object storage without proxying bytes through your server, the mechanism is the same everywhere: your backend mints a short-lived presigned URL, the browser PUTs to it. What differs is where the cost lands. On Infrai, POST /v1/storage/object/presign/{bucket}/{key} is free and rate-limited, so the coordination calls cost nothing — you pay per write on PUT /v1/storage/object/put/{bucket}/{key}, and new accounts get trial credit that covers the first several thousand of them.

Deliberately no rate quoted here: storage pricing moves, and a number pinned into a page like this is wrong within a quarter. The structure is what’s durable, and the structure is what decides your bill — for most upload workloads the per-write price is not the dominant term anyway.

Mint the URL server-side

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST \
  "https://api.infrai.cc/v1/storage/object/presign/user-uploads/avatars/tenant_42/photo.jpg" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "op": "put",
    "expires_seconds": 900,
    "content_type": "image/jpeg",
    "max_bytes": 5242880
  }'

The documented body is op, expires_seconds, content_type and max_bytes; the response is {url, method, headers, fields, expires_at, max_bytes}.

max_bytes is the field that earns its place. The ceiling is enforced on the upload itself, so a client can’t turn your 5 MB avatar endpoint into a 5 GB one — you don’t have to trust the browser, and you don’t need a separate abuse check.

The full round trip

const BASE = "https://api.infrai.cc";
const BUCKET = "user-uploads";

/** Server side. Never hand the API key to a browser — hand it a URL that expires. */
export async function presignUpload(tenant: string, filename: string, contentType: string) {
  const key = `avatars/${tenant}/${crypto.randomUUID()}-${filename.replace(/[^\w.-]/g, "_")}`;
  const res = await fetch(`${BASE}/v1/storage/object/presign/${encodeURIComponent(BUCKET)}/${key}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.INFRAI_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ op: "put", expires_seconds: 900, content_type: contentType, max_bytes: 5 * 1024 * 1024 }),
  });
  if (!res.ok) throw new Error(`presign failed ${res.status}: ${await res.text()}`);
  return { key, ...(await res.json()) };
}
// Browser side. The upload never touches your server.
async function uploadFile(file) {
  const { url, method, headers, key } = await fetch("/api/presign", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ filename: file.name, contentType: file.type }),
  }).then((r) => r.json());

  const put = await fetch(url, {
    method: method ?? "PUT",
    headers: { ...(headers ?? {}), "Content-Type": file.type },
    body: file,
  });

  if (!put.ok) throw new Error(`upload failed: ${put.status}`);
  return key;                          // persist this; it is the object's identity
}

Confirm it landed without paying for a download:

curl -sS "https://api.infrai.cc/v1/storage/object/head/user-uploads/avatars/tenant_42/photo.jpg" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

GET /v1/storage/object/head/{bucket}/{key} returns {found, size_bytes, etag, content_type, last_modified}. Note the key contains slashes — that’s normal, the key occupies several path segments.

What actually drives the bill

This is where most comparisons mislead. For browser uploads the per-write price is rarely the dominant term:

  • Presigning is free, so coordination is not a cost line at all.
  • Writes are billed per call, and the trial credit covers a few thousand before you pay.
  • Reads are billed per call at roughly double the write rate — that ratio is the part worth internalising, because most files are read far more often than they’re written.
  • Egress is metered, and STORAGE_BANDWIDTH_EXCEEDED is what a hot public asset looks like when it’s served straight from the bucket.

For today’s actual rates, ask the API rather than trusting any article — including this one:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | node -e 'const d=JSON.parse(require("fs").readFileSync(0,"utf8"));
             for (const c of d.capabilities.filter(x=>x.namespace==="storage"&&x.billing?.is_billable))
               console.log(c.method, c.path, c.billing.price_usd, c.billing.unit);'

So if your files are written once and read constantly — avatars, product images, public documents — read volume and egress decide the cost, and the write price is noise. Put a CDN in front and the picture changes again.

GET /v1/storage/bucket/usage/{bucket} is free and reports what a bucket actually holds. Check it before modelling anything.

Honest comparison

OptionWhere it winsThe catch
Infrai presign + putPresigning free; the bucket sits on the same key and bill as your queue, email and AI usageNot a CDN; no read-time image transformation; egress metered
Cloudflare R2Zero egress fees — decisively cheapest for public, read-heavy assetsAnother vendor and bill; you wire up your own account and keys
AWS S3Deepest ecosystem, lifecycle rules, IAM, every tool integrates with itEgress is the classic cost surprise; IAM is real work for a small team
Supabase StorageBundled with Postgres and auth if you’re already on SupabaseOnly compelling as part of that stack

If your workload is overwhelmingly public reads of static files, R2 will be cheaper than Infrai and it isn’t close — zero egress beats any per-call rate once bytes served dominates. Take that option and don’t overthink it.

Infrai’s argument is narrower and it’s about consolidation, not unit price: presigning is free, writes are cheap, and the storage lives on the same account as the queue job that processed the upload, the image route that made the thumbnail and the email that told the user it was ready. One key, one bill, one place to attribute a tenant’s cost. If storage is the only thing you need, buy storage from a storage company.

Two limitations worth knowing before you commit

The presigned URL has an expiry you set, and there’s no server-side resumability on the simple path — a dropped connection means the browser starts that file again. For large files use POST /v1/storage/multipart/create/{bucket}, which is free and returns part_size_min and part_count_max; presign each part and complete when they’re all up.

And this API doesn’t transform on read. If you need ?width=400 in a URL, that’s a separate POST /v1/image/process step at upload time, or a specialist like Cloudinary.

References

Browse more storage developer guides