What's the best object storage for a SaaS? (September 2026 Comparison)
For most SaaS apps, Upstash Blob is the best default for storing images, videos, and any other file. The first 1 TB of downloads each month is free, so most apps pay no egress, same as on Cloudflare R2. On top of that you get a global CDN, private buckets with signed URLs, browser uploads through its SDK, and the S3 API.
If your users download many terabytes a month, Cloudflare R2 is cheaper because its egress stays free. S3 is a good fit if your team already runs on AWS.
To compare the options, let's see some examples!
What makes storage expensive?
Usually, most storage cost comes from egress (the bytes your users download) and from the number of requests.
For example, let's say your app stores 50 GB and your users download 500 GB a month. S3 charges $0.023/GB for storage and $0.09/GB for egress, with the first 100 GB of egress free each month:
Storage: 50 GB × $0.023 = $1.15
Egress: (500 − 100) GB × $0.09 = $36.00Egress costs 31 times more than storage here!
Or on Cloudflare, hosting 100,000 objects of about 100 KB, read 10 million times a day would costs $104.40 a month on R2, all of it from read requests. The 10 GB of storage is included within the free tier.
Storage and egress prices also vary a lot between providers. R2 charges $0.015/GB for storage and nothing for egress. S3 charges less than twice that for storage and $0.09/GB to download. So a low storage rate doesn't tell you much until you know how often your files get downloaded.

What should you compare besides price?
To get a good estimate on how much storage will cost, we need to look at Egress, request pricing and how much the free tier includes.
Also CDN, regions, S3 compatibility, access control and upload tooling decide how much work you need to do yourself:
- Egress rate. It ranges from $0/GB on R2 to $0.09/GB on S3, and Upstash Blob pay-as-you-go serves the first 1 TB a month for free. Egress is usually the most expensive for a project with many downloads.
- Request pricing. Writes are a lot more expensive than reads. Upstash Blob charges $4.50 per million uploads and lists, and $0.30 per million reads. An app that saves many small files pays mostly for writes.
- Free tier. The Upstash Blob free plan gives 1 GB of storage and 10 GB of bandwidth a month.
- CDN. Files load fast worldwide only if a CDN caches them. Upstash Blob serves public files through a fast, global CDN at no extra fee. On S3 you add CloudFront yourself. Vercel Blob doesn't cache files over 512 MB, so those files cost origin transfer on every download.
- Regions. S3 prices and latency depend on the region you pick. An Upstash Blob bucket has no region (because it's globally distributed by default): one global bucket, one rate sheet, and no cross-region transfer fees.
- S3 compatibility. If a provider supports the S3 API, you can switch later with the AWS SDK, the AWS CLI or any S3 tool. S3 and R2 support it natively, and Upstash Blob hands out temporary S3 credentials for the same bucket.
- Access control. Tenant files need signed URLs: short-lived links that work for one file and one action. Private buckets should have no public URL at all.
- Uploads. On serverless functions, you can only send very limited request body sizes, so it's better to upload files from the browser with a presigned URL. Some SDKs sign these uploads and split big files for you. With others you write that code yourself. This post on file uploads in React covers that flow in detail.
How should a multi-tenant SaaS organize its files?
A multi-tenant SaaS works best with one bucket and one path prefix per tenant, like tenants/acme/. Your server picks the path from the logged-in session, and users read files through short-lived signed URLs.
AWS describes the same shared-bucket, prefix-per-tenant pattern for S3, with a policy that limits each tenant to its own prefix. This layout works on any provider, because a prefix is just part of the file's name.
A separate bucket per tenant keeps each customer fully isolated and gives you a separate cost line for each one. But you'll run into limits. S3 gives each account 2,000 free buckets and then charges $0.02 per extra bucket a month. Vercel Blob allows 100 stores on Hobby and 1,000 on Enterprise. You can create as many prefixes as you want.

On Upstash Blob, the server builds the path, uploads a file, lists one tenant's files, and creates a signed read link:
import { Bucket } from "@upstash/blob";
const bucket = Bucket.fromEnv();
function tenantPath(tenantId: string, fileName: string) {
return `tenants/${tenantId}/${fileName}`;
}
const run = crypto.randomUUID().slice(0, 8);
const path = tenantPath("acme", `${run}/invoice-001.pdf`);
try {
await bucket.put(path, "Acme invoice 001 (demo text)", { contentType: "text/plain" });
const page = await bucket.list({ prefix: tenantPath("acme", `${run}/`) });
console.log("Keys:", page.blobs.map((blob) => blob.path));
const { expiresAt } = await bucket.signedReadUrl(path, { expiresIn: "5m" });
console.log("expiresAt:", expiresAt.toISOString());
} finally {
await bucket.del(path);
}If we run it on a live bucket, we get the key and the link's expiry time:
Keys: [ 'tenants/acme/1b209fa0/invoice-001.pdf' ]
expiresAt: 2026-09-23T08:39:49.107ZA list call can only filter by prefix. You can't ask the bucket for "files owned by user 7" or "files from last week". That's why it's a good idea to keep our own table of which tenant owns which path and only use the bucket to store the files.
Also to illustrate a tenant boundary, what happens if we take a signed link for one tenant's invoice and changed the path in the URL to another tenant's file?
On Upstash Blob (as we'd expect), the request is denied:
Signed GET: 200; body: Acme invoice 001 (demo text)
Swapped-path GET: 403
Bucket: public
Unsigned GET: 200Now, the test bucket was public, so the URL without a signature still returned the file. A private bucket has no public URL, so every read needs a signed link from your server. You can choose between a public or private bucket when creating one, and tenant documents should definitely (!) go in a private one. Public buckets are good for avatars and product images, anything other people are allowed to see.

Browser uploads work the same way. Each presigned URL covers one object, one method, one set of headers and a few minutes. Your server's upload handler picks the path, so a browser can't write into another tenant's prefix. The SDK also refuses any path with a .. segment to block path-traversal attempts.
How do Upstash Blob, R2, S3 and Vercel Blob compare?
Below 1 TB of downloads a month, neither Upstash Blob nor Cloudflare R2 charges for egress, so the bill comes down to storage and requests. R2 is the cheapest in both examples below, because its free tier covers 10 GB of storage and the first 10M reads. Upstash Blob costs a few dollars more for a typical app and includes a CDN, signed uploads and one global bucket. S3 costs the most once users download a lot. Vercel Blob's rates are higher than Upstash Blob's on every line.
| Provider | Storage per GB | Egress per GB | Writes per 1M | Reads per 1M | Free each month |
|---|---|---|---|---|---|
| Upstash Blob | $0.02 | $0 up to 1 TB, then $0.02 | $4.50 | $0.30 | 1 GB storage, 10 GB bandwidth (free plan); 1 TB egress (pay as you go) |
| Cloudflare R2 | $0.015 | $0 | $4.50 | $0.36 | 10 GB storage, 1M writes, 10M reads |
| AWS S3 Standard | $0.023 | $0.09 | $5.00 | $0.40 | 100 GB egress |
| Vercel Blob | $0.023 | $0.05 | $5.00 | $0.40 | 1 GB storage, 10 GB transfer (Hobby) |
Upstash Blob counts uploads, copies and lists as writes, and downloads and HEAD requests as reads. Deletes are free.

Rates are easier to compare on a real workload, so let's see some examples.
Example 1: A document SaaS stores 200 GB and serves 300 GB a month, with 1 million uploads and 10 million reads:
Upstash Blob: 4.00 storage + 0 egress (under 1 TB) + 4.50 writes + 3.00 reads = $11.50
R2: 2.85 storage (190 GB billed) + 0 + 0 + 0 (free tier) = $2.85
S3: 4.60 storage + 18.00 egress (200 GB billed) + 5.00 + 4.00 = $31.60Example 2: A media SaaS stores 1 TB and serves 10 TB a month, with 2 million uploads and 50 million reads:
Upstash Blob: 20.00 + 180.00 egress (9,000 GB billed) + 9.00 + 15.00 = $224.00
R2: 14.85 + 0 egress + 4.50 (1M billed) + 14.40 (40M) = $33.75
S3: 23.00 + 891.00 egress (9,900 GB) + 10.00 + 20.00 = $944.00R2 is the cheapest for both apps. In the document SaaS, most of the $8.65 gap comes from R2's free tier covering all the requests. In the media SaaS, $180 of the $190.25 gap is egress past Upstash Blob's free 1 TB. S3 costs the most in both, because it charges egress on every download past the free 100 GB.

Upstash Blob supports the S3 API, so you can use the AWS SDK with any Upstash bucket. This also means you can move to and from another provider with standard S3 tools:
import { Bucket } from "@upstash/blob";
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
const bucket = Bucket.fromEnv();
const s3cfg = bucket.s3();
const s3 = new S3Client({ endpoint: s3cfg.endpoint, region: s3cfg.region, credentials: s3cfg.credentials });
const path = `tenants/acme/${crypto.randomUUID().slice(0, 8)}/temp.txt`;
try {
await bucket.put(path, "S3 listing demo", { contentType: "text/plain" });
const page = await s3.send(new ListObjectsV2Command({ Bucket: s3cfg.bucket, Prefix: "tenants/", MaxKeys: 5 }));
console.log("Keys:", (page.Contents ?? []).map((object) => object.Key));
console.log("KeyCount:", page.KeyCount);
} finally {
s3.destroy();
await bucket.del(path);
}The file we wrote with the Upstash SDK shows up when we list with the AWS SDK:
Keys: [ 'tenants/acme/7afb02f9/temp.txt' ]
KeyCount: 1What's the best object storage for Next.js?
For most Next.js apps, Upstash Blob is the best object storage: the @upstash/blob SDK includes a Next.js route handler and a React hook for uploads, and the first 1 TB of egress each month is free.
Vercel Functions reject request bodies over 4.5 MB, so larger files have to go from the browser straight to storage. On R2 or S3 you write the presigned-URL route and the multipart logic yourself, or add UploadThing on top. With Upstash Blob, the upload handler authorizes each upload, checks its size and file type, and records the file once it lands:
// app/api/upload/route.ts
import { uploadHandler } from "@upstash/blob";
export const { GET, POST } = uploadHandler({
constraints: { maxSize: "20mb", contentTypes: ["image/*", "application/pdf"] },
onBeforeUpload: ({ file }) => ({ path: `images/${file.name}` }),
});On the client, useUpload() from @upstash/blob/react gives you the file picker's accept list, the upload progress and the finished file's URL. Files over 16 MB upload in parts, with pause, resume and retries, and the bytes never pass through your Next.js server.
Upstash Blob works the same way on Vercel, a VPS or Cloudflare Workers, so moving a Next.js app off Vercel doesn't mean moving its files. Vercel Blob's rates are higher than Upstash Blob's on every line of the table above.
Which object storage should you use?
For most SaaS apps, pick Upstash Blob, and move to R2 when downloads reach many terabytes a month:
- Most SaaS apps (user uploads, tenant documents, AI-generated files, under 1 TB of downloads a month): Upstash Blob. Egress is free up to 1 TB, public files are served from a global CDN, and private files stay behind signed reads. Its SDK signs browser uploads, and one bucket serves every region. It fits Next.js and any serverless stack. Pay-as-you-go starts at $0.02/GB.
- If downloads are your biggest cost: Cloudflare R2 is the cheapest fit. Video, large media and public datasets pay $0 egress there, and R2 cost $33.75 against $224 on Upstash Blob in the 10 TB example.
- Your team already runs on AWS: S3 with CloudFront in front is a good fit. IAM (AWS's permission system) policies can lock each tenant to its own prefix, so tenant isolation fits the tools your team already uses.
Whichever one you pick, the tenant layout from earlier works the same way (one bucket, a prefix per tenant, and short-lived signed URLs). Because all three support the S3 API, you can copy a bucket to another provider later with standard S3 tools.
This guide to file uploads in a React app walks through building the upload flow, from the browser straight to storage, step by step.
