file storage · side by side
Cloudflare R2 vs Vercel Blob for a Next.js app
Both fill the file storage slot, so a generated repo carries one or the other, never both. Every line below is read out of the two manifests.
Short answer
Pick Cloudflare R2 if
Anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again.
Pick Vercel Blob if
Apps already on Vercel that want user uploads (avatars, attachments, imports) without adding a cloud account, IAM, CORS rules or a second bill. Strongest when files are small to medium and read by their owner.
Side by side
Price, obligations, and the surface each one adds. No row is written by hand. This is manifest.yaml, rendered.
| From the manifest | Option ACloudflare R2 | Option BVercel Blob |
|---|---|---|
| In one line | Cloudflare R2 S3-compatible object storage with free egress. You pay for storage and requests. | Vercel Blob File storage inside your Vercel project. One env var to wire, no second vendor. |
| Pricing | Cloudflare R2 Free each month: 10 GB stored, 1M Class A and 10M Class B operations. Then $0.015 per GB-month, $4.50 per million Class A operations (writes, lists) and $0.36 per million Class B operations (reads). Egress is free. | Vercel Blob Hobby: free up to 1 GB stored, 10,000 simple and 2,000 advanced operations, and 10 GB of transfer a month. Past the limit, Blob stops until the 30 days reset. Pro bills by usage. Prices below are for iad1. Storage is $0.023 per GB-month and transfer $0.05 per GB. Simple operations (cache-miss reads) are $0.40 per million. Advanced operations (put, copy, list) are $5 per million. Deletes are free. Client uploads have no transfer charge. |
| Best for | Cloudflare R2 Anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again. | Vercel Blob Apps already on Vercel that want user uploads (avatars, attachments, imports) without adding a cloud account, IAM, CORS rules or a second bill. Strongest when files are small to medium and read by their owner. |
| Trade-offsVerbatim from the manifest | Cloudflare R2
| Vercel Blob
|
| Required companionsAdded for you, with a reason | Cloudflare R2 Nothing. It stands on its own. | Vercel Blob Nothing. It stands on its own. |
| Recommended alongsideSuggested, never added for you | Cloudflare R2
| Vercel Blob
|
| Env vars you will manageEvery one documented in docs/onboard.md | Cloudflare R2 7 variables · 4 required
| Vercel Blob 4 variables · 1 required
|
| Dependencies added | Cloudflare R2
| Vercel Blob
|
| MCP serversWritten into .mcp.json | Cloudflare R2 None. No extra agent tools from this one. | Vercel Blob None. No extra agent tools from this one. |
| Footprint in your repo | Cloudflare R2 13 files, plus 3 injections into shared stack files | Vercel Blob 13 files, plus 3 injections into shared stack files |
What changes in your repo
The paths each battery contributes, diffed. A path in the third list is written by both, so swapping rewrites that file rather than adding one.
Only with Cloudflare R2 (3)
infra/2 files
r2/2 files
- cors.json
- lifecycle.json
scripts/1 file
r2/1 file
- apply-config.ts
Only with Vercel Blob (3)
src/2 files
lib/2 files
storage/2 files
- access.ts
- confirm.ts
variants/1 file
auth-clerk/1 file
slots/1 file
- auth-public-routes.ts
Same path, different implementation (10)
src/5 files
app/1 file
api/1 file
upload/1 file
- route.ts
components/1 file
upload/1 file
- file-dropzone.tsx
lib/3 files
storage/3 files
- authorize.ts
- index.ts
- keys.ts
tests/1 file
unit/1 file
- storage-keys.test.ts
variants/4 files
auth-none/1 file
src/1 file
lib/1 file
storage/1 file
- uploader.ts
auth-wired/1 file
src/1 file
lib/1 file
storage/1 file
- uploader.ts
errors-none/1 file
src/1 file
lib/1 file
storage/1 file
- report.ts
errors-sentry/1 file
src/1 file
lib/1 file
storage/1 file
- report.ts
Shared stack files Cloudflare R2 injects into
- env-required
- legal-processors
- verify-checks
Shared stack files Vercel Blob injects into
- env-required
- legal-processors
- verify-checks
Cloudflare R2 in your .env.local
# required
R2_ACCESS_KEY_ID=0123456789abcdef0123456789abcdef
R2_ACCOUNT_ID=0123456789abcdef0123456789abcdef
R2_BUCKET=uploads
R2_SECRET_ACCESS_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
# optional
R2_PUBLIC_BASE_URL=https://files.example.com
STORAGE_ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com
STORAGE_DEV_UPLOADER=dev-user
Vercel Blob in your .env.local
# required
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_0123456789abcdef_0123456789abcdef0123456789abcdef
# optional
STORAGE_ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com
STORAGE_DEV_UPLOADER=dev-user
VERCEL_BLOB_CALLBACK_URL=
What changes for your agents
Each battery ships rules, skills, subagents and hooks that an agent loads before it touches the code that battery owns. Picking one is also picking how your agents behave in src/lib/storage/**.
Cloudflare R2
2
Skills
2
Rules
5
Solution docs
Rules (2)
Bucket configuration is code, and R2 is S3-compatible rather than S3
infra/r2/** · scripts/r2/** · src/lib/storage/**
Never sign a URL without an authorisation check, and never proxy uploads through a route
src/lib/storage/** · src/app/api/upload/** · src/components/upload/**
Skills (2)
/add-bucket
Add a second R2 bucket for a different access model, with its CORS and lifecycle configuration committed as code and applied by script rather than clicked into the dashboard.
/upload-flow
Add a complete file upload to a feature: authorised signing route, direct-to-R2 PUT, the database row that records the key, and the cleanup that stops orphans.
Subagents and hooks
None of its own. The foundation agents and guard hooks still ship.
Vercel Blob
2
Skills
2
Rules
7
Solution docs
Rules (2)
Vercel Blob store - access mode, credentials, cache, cost
src/lib/storage/** · scripts/verify.ts
Client uploads - auth before the token, the server names the key, the token is locked down
src/lib/storage/** · src/app/api/upload/** · src/components/upload/**
Skills (2)
/add-upload-kind
Add a new kind of upload (avatars, attachments, imports) to the Vercel Blob flow, with its own folder, type and size limits, the row that owns the key, and the cleanup that stops orphans.
/clean-orphaned-blobs
Find and delete Vercel Blob objects no database row points at, safely, in batches, without deleting uploads that are still in flight.
Subagents and hooks
None of its own. The foundation agents and guard hooks still ship.
What each one already knows
Solution docs land in docs/solutions/ in your repo and are published here, so you can read the failure modes before you commit.
Cloudflare R2 (5)
- Orphaned objects in R2: lifecycle rules and the sweep they cannot doDirect-to-bucket uploads leak objects nobody references. Lifecycle rules clean up a tmp/ prefix and abandoned multipart parts; owner-scoped orphans need a reconciliation job.docs/solutions/r2/cleaning-up-orphaned-uploads.md
- The R2 upload that fails in the browser and works in curl: bucket CORSA presigned PUT from a page is a cross-origin request. Without CORS rules on the bucket it fails with an error that never says CORS, and the signature gets blamed.docs/solutions/r2/cors-on-a-bucket.md
- Serving R2 objects publicly: custom domains, r2.dev and cache headersr2.dev is rate-limited and not for production. A custom domain puts Cloudflare's cache in front of the bucket, but only if you wrote Cache-Control at upload time.docs/solutions/r2/custom-domains-and-cache-headers.md
- Presigned PUT or multipart: picking an upload strategy for R2A single presigned PUT is right up to about 100 MB and restarts from zero when it fails. Above that, multipart is not an optimisation, it is the only thing that works.docs/solutions/r2/presigned-put-vs-multipart.md
- Zero egress fees, and the workloads where R2 actually beats S3Egress is the line that surprises people on an S3 bill. R2 charges nothing for it and charges for operations instead: here is the arithmetic for deciding, including where R2 loses.docs/solutions/r2/zero-egress-and-when-r2-beats-s3.md
Vercel Blob (7)
- Vercel Blob addRandomSuffix, allowOverwrite, and stale files in the cacheBlob refuses to overwrite by default, and an overwrite can take 60 seconds to show plus whatever the browser cached. Treat blobs as immutable; use new pathnames, not overwrites.docs/solutions/vercel-blob/addrandomsuffix-allowoverwrite-and-the-cache.md
- Vercel Blob: client uploads vs server uploads and the 4.5 MB limitA Vercel Function accepts at most 4.5 MB of request body. Server uploads hit it; client uploads skip it. How client uploads work, and when a server upload is still right.docs/solutions/vercel-blob/client-uploads-and-the-4-5-mb-body-limit.md
- Deleting orphaned blobs in Vercel BlobBlobs nobody references still bill every month. Where orphans come from, how to delete them in the same code path as the row, and a safe sweep with list() and del() for the rest.docs/solutions/vercel-blob/deleting-orphaned-blobs.md
- Vercel Blob handleUpload: never trust the pathname from the clientWith client uploads the browser names the pathname and the token is bound to it. Check it in onBeforeGenerateToken, or any signed-in user can write anywhere in your store.docs/solutions/vercel-blob/never-trust-the-client-pathname.md
- Why Vercel Blob onUploadCompleted never fires on localhostonUploadCompleted is a webhook from Vercel to your app. It cannot reach localhost, so the SDK skips it. Use a tunnel and VERCEL_BLOB_CALLBACK_URL, and never make the upload depend on it.docs/solutions/vercel-blob/onuploadcompleted-never-fires-on-localhost.md
- Private vs public Vercel Blob stores: picking one you cannot changeA Blob store's access mode is fixed at creation. Private needs a credential for every read; public is readable by anyone with the URL. How to serve each, and why user files belong in private.docs/solutions/vercel-blob/private-vs-public-blob-stores.md
- What Vercel Blob actually costs, and where egress hidesStorage is cheap. Advanced operations and data transfer are the lines that grow. How each is counted, why private delivery can cost more, and the habits that keep the bill flat.docs/solutions/vercel-blob/vercel-blob-costs-and-egress.md
Which one to pick
From meta.bestFor and meta.tradeoffs. If a claim is not in the manifest, it is not on this page.
Pick Cloudflare R2 when
Anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again.
And accept that(6)
- No egress fee, but operations are billed. Many tiny reads can cost more than the bytes. Check Class B pricing against your access pattern, not just your storage volume.
- S3-compatible, not S3. The common surface works with the AWS SDK. Parts of the long tail (some checksum modes, ACLs, object lock, storage classes) do not. Test, do not assume.
- Authorisation is your job. R2 has no row level security, so "may this user read this key" is a decision your app makes on every request.
- A bucket is private until you attach a custom domain or enable the r2.dev subdomain, and r2.dev is rate-limited and not for production. Public serving means DNS work, not a checkbox.
- Strong read-after-write consistency, but no built-in image transforms. That is a separate Cloudflare product, or a resize on upload in your own code.
- Bucket config (CORS, lifecycle) lives in Cloudflare, not in your migrations. It needs its own committed files and a command to apply them. This battery ships both.
Pick Vercel Blob when
Apps already on Vercel that want user uploads (avatars, attachments, imports) without adding a cloud account, IAM, CORS rules or a second bill. Strongest when files are small to medium and read by their owner.
And accept that(6)
- Egress is billed. Blob data transfer is cheaper than Vercel's regular CDN transfer, but it is not zero. Large media served at volume is the case R2 exists for.
- A store is private or public forever. You pick at creation and cannot change it. Mixing both means two stores.
- Advanced operations are the expensive line. Every put, copy and list counts, and so does browsing the store in the Vercel dashboard.
- Authorisation is your code. Blob has no row level security. The upload route decides who may write and where, every time.
- onUploadCompleted is a webhook from Vercel to your app. It cannot reach localhost, so local testing needs a tunnel.
- Overwrites and deletes take up to 60 seconds to leave the cache. Treat blobs as immutable and give every version a new pathname.
Questions people actually ask
- Should I choose Cloudflare R2 or Vercel Blob?
- Cloudflare R2 is best for anything read far more often than it is written, and anything large. User uploads, video, model weights, datasets, backups you may need to pull out again. Vercel Blob is best for apps already on Vercel that want user uploads (avatars, attachments, imports) without adding a cloud account, IAM, CORS rules or a second bill. Strongest when files are small to medium and read by their owner. Both fill the file storage slot, so a generated repo carries one or the other, never both.
- How much do Cloudflare R2 and Vercel Blob cost?
- Cloudflare R2: Free each month: 10 GB stored, 1M Class A and 10M Class B operations. Then $0.015 per GB-month, $4.50 per million Class A operations (writes, lists) and $0.36 per million Class B operations (reads). Egress is free. Vercel Blob: Hobby: free up to 1 GB stored, 10,000 simple and 2,000 advanced operations, and 10 GB of transfer a month. Past the limit, Blob stops until the 30 days reset. Pro bills by usage. Prices below are for iad1. Storage is $0.023 per GB-month and transfer $0.05 per GB. Simple operations (cache-miss reads) are $0.40 per million. Advanced operations (put, copy, list) are $5 per million. Deletes are free. Client uploads have no transfer charge.
- What changes in my repo if I switch from Cloudflare R2 to Vercel Blob?
- Cloudflare R2 writes 13 files, 7 environment variables and 3 dependencies, and installs 2 path-scoped rules, 2 skills and 5 solution docs. Vercel Blob writes 13 files, 4 environment variables and 2 dependencies, and installs 2 path-scoped rules, 2 skills and 7 solution docs.
Decide once, then build the repo that already knows the decision.
Either way you get that choice’s rules, skills and solution docs installed, plus the guard hooks, an onboarding doc for exactly these env vars, and the Compound Engineering loop. Free and MIT.