
Vercel Blob is a managed object-storage service built on Cloudflare R2 that lets a Next.js or any Node.js app upload, store, and serve files behind a global CDN with zero infrastructure setup. You install @vercel/blob, generate a read-write token in the Vercel dashboard, and either put() files from a server action or hand the browser a signed URL for direct client-side uploads. Storage costs $0.15/GB-month and egress to non-Vercel destinations costs $0.30/GB.
That's the short version. The longer version is where the trade-offs live: when to pick Blob over raw S3, how to wire up client-side uploads without leaking your token, and how pricing compounds once you have real traffic.
For a decade, "use S3" was the answer to every file-storage question. AWS sets the floor on cost, gives you fine-grained IAM, and pairs with CloudFront for delivery. That's still true at scale.
What changed: most new SaaS apps ship on Vercel, Cloudflare Workers, or Fly, with teams of 2 to 5 engineers. The cost of configuring S3 (bucket, CORS, IAM, signed URLs, CloudFront, signed cookies for private content) is now larger than the cost of the storage for the first 18 months. Vercel Blob is the response: a thin product wrapper on Cloudflare R2 that trades a price premium for zero config.
Vercel Blob is a managed bucket per project (or per Vercel team) backed by Cloudflare R2 storage, with a @vercel/blob SDK and an HTTP API on top. Every uploaded file gets a unique public URL on *.public.blob.vercel-storage.com and is served through Cloudflare's edge network.
Key facts:
@vercel/blob, works in Node and Edge runtimes.BLOB_READ_WRITE_TOKEN env var grants full bucket access. Don't expose it client-side.The mental model: an S3 bucket your serverless functions can write to with one line, served over a CDN with no auth dance.
The Vercel Blob pricing page lists three line items. They behave very differently as your app grows.
| Cost component | Rate (2026) | When it bites |
|---|---|---|
| Storage | $0.15 / GB-month | Linear with total stored data. Predictable. |
| Egress to non-Vercel destinations | $0.30 / GB | Spikes if you serve videos or large downloads outside the Vercel CDN. |
| Egress to Vercel-hosted apps | Free | If your Next.js app pulls a Blob through <Image>, no egress charge. |
| Advanced operations | $5 / 1M ops | List, copy, head, multipart upload init. Hits hard if you do recursive sync. |
| Simple operations | Included in storage | put, get, del on individual objects. |
A worked example. Store 500 GB of user uploads, serve 2 TB/month to browsers (egress to non-Vercel because the browser is not a Vercel server):
The same workload on S3 + CloudFront: ~$11.50 storage + $170 egress = **$182/month**. That's a 3.7x premium for Blob. For 50 GB stored and 200 GB egress, the gap is closer to $67 vs $19. The crossover point is around 200 GB of monthly egress; past that, you're paying real money for convenience.
Most teams' first instinct on Vercel is to reach for S3 anyway, because that's what they know. They npm install @aws-sdk/client-s3, write a presigned-URL endpoint, handle CORS, set up CloudFront, configure cache headers, then spend a Friday afternoon debugging why their signed URLs return 403 in Safari.
This works. It's also four to six hours of yak-shaving that produces zero user-facing value on day one. For the first year of a product's life, every hour spent on storage plumbing is an hour not spent on the feature that determines whether the company exists in year two.
The flip side: teams that pick Blob and never check the bill are in for a surprise when a viral product launch pushes 5 TB of egress in a week.
Here's the playbook we use on Cadence client projects.
npm install @vercel/blob
In the Vercel dashboard, go to your project, then Storage, then Create a Blob store. Vercel writes BLOB_READ_WRITE_TOKEN into your environment for all three deploy targets (production, preview, development). Run vercel env pull so local dev picks it up.
For anything under 4.5 MB (avatars, thumbnails, generated PDFs), upload directly from a Next.js Server Action:
'use server';
import { put } from '@vercel/blob';
export async function uploadAvatar(formData: FormData) {
const file = formData.get('file') as File;
if (!file) throw new Error('No file provided');
const blob = await put(`avatars/${crypto.randomUUID()}-${file.name}`, file, {
access: 'public',
addRandomSuffix: false,
contentType: file.type,
});
return { url: blob.url, pathname: blob.pathname };
}
The returned blob.url is a public CDN URL you can drop into a database, render with <Image>, or expose in an API response.
Why it matters: this is one function call. There's no presigned URL flow, no CORS preflight, no separate cache configuration.
What can go wrong: if file.size > 4.5 * 1024 * 1024, the Vercel function rejects the request body before your code runs. You'll see a generic 413. Switch to the client-upload path below for anything that might exceed it.
For uploads larger than 4.5 MB (videos, design files, dataset CSVs), the browser uploads directly to Blob's edge, and your server only signs the request. The pattern uses @vercel/blob/client:
// app/api/upload/route.ts
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
export async function POST(request: Request) {
const body = (await request.json()) as HandleUploadBody;
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
return {
allowedContentTypes: ['image/*', 'video/mp4', 'application/pdf'],
maximumSizeInBytes: 500 * 1024 * 1024,
tokenPayload: JSON.stringify({ userId: session.user.id }),
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
const { userId } = JSON.parse(tokenPayload!);
await db.uploads.insert({ userId, url: blob.url, pathname: blob.pathname });
},
});
return NextResponse.json(jsonResponse);
}
On the client:
'use client';
import { upload } from '@vercel/blob/client';
async function handleFile(file: File) {
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: '/api/upload',
});
return blob.url;
}
The browser bypasses your serverless function entirely on the data path. Your function only handles two small JSON round-trips: one to mint a signed token, one webhook when the upload finishes.
What can go wrong: skipping the auth check inside onBeforeGenerateToken is the single most common mistake. Without it, anyone on the internet can upload to your bucket. Always tie the token to a real session.
If you're rendering images, wrap the Blob URL in Next.js's <Image> component. Vercel sees the request, recognizes the Blob domain, and counts the egress as Vercel-to-Vercel (free). If you hotlink the raw Blob URL from a static HTML page hosted elsewhere, you're paying $0.30/GB.
In the Vercel dashboard, go to Settings, then Spend Management, and set a soft alert at 50% of your budget and a hard cap at 100%. Vercel will email you and (at the hard cap) start rejecting Blob requests rather than running up a $40k bill overnight. Founders who skip this step are the ones with the horror stories. The same discipline applies when you roll out feature flags on anything that touches large files.
If you hit 500 GB stored or 1 TB monthly egress, start modeling the cost on raw R2 (which Blob sits on top of) or S3 plus CloudFront. The migration tool we like is rclone, which can mirror a Blob bucket to S3 in a few hours for a typical mid-stage SaaS. Don't migrate prematurely; the rule of thumb is "the storage bill has to be larger than the engineer-week it costs to switch."
| Scenario | Pick |
|---|---|
| You're shipping a Next.js app on Vercel and need file uploads next week | Vercel Blob |
You serve mostly images through <Image> on the same Vercel project | Vercel Blob |
| You need fine-grained IAM (per-user buckets, cross-account access) | S3 |
| You have >1 TB stored or >2 TB monthly egress | S3 + CloudFront |
| You need lifecycle policies (auto-delete after N days, glacier tier) | S3 |
| You're already on AWS for compute and want one bill | S3 |
| You need S3 Object Lock for compliance (legal hold) | S3 |
| Team size: 2 to 5 engineers, no platform engineer | Vercel Blob |
| You want signed cookies for an entire path, not per-file URLs | S3 + CloudFront |
| You're building a HIPAA-regulated app and need a BAA | S3 (Vercel Blob doesn't sign BAAs as of 2026) |
The HIPAA point is non-obvious and worth its own post; if that's your situation, start with our guide on HIPAA SaaS design from day one.
A few things bite teams in production:
BLOB_READ_WRITE_TOKEN to the browser. Anything prefixed with NEXT_PUBLIC_ is bundled into the client. The token is read-write on your entire bucket. A leaked token means anyone can upload anything, including content that gets you delisted from app stores.addRandomSuffix: false. By default, Blob appends a random suffix to your pathname. If you're using deterministic filenames for cache busting, this breaks your invalidation.Cache-Control. Vercel sets a sane default, but if you're serving versioned assets you want Cache-Control: public, max-age=31536000, immutable. Pass it via the cacheControlMaxAge option to put().list() calls in cron jobs. list is an advanced operation. Calling it across 100k objects every five minutes will surprise you on the bill. Cache the listing or paginate explicitly.Cloudflare R2 replicates objects across multiple zones in a single region, with 11 nines of durability. There is no multi-region replication out of the box. If you need disaster recovery against a full Cloudflare region failure, roll your own mirror to a second provider, or accept that R2's track record means this is unlikely to matter early on.
For most SaaS startups, this is fine. For a fintech or healthtech app where file unavailability is a regulatory incident, plan a cross-provider mirror from day one.
If your app stores fewer than 100 files total (a marketing site, a B2B SaaS with occasional logo uploads), you don't need a file-storage strategy. Drop assets in your Git repo or use Vercel's static hosting. The Blob discussion only matters once user-generated content is real.
Same goes for apps where the file is the product (a video platform, a CAD tool). Those should be on raw R2 or S3 from day one because the storage bill will dwarf everything else.
Token setup, server actions, client-upload routes, and egress alerts are a four-to-six-hour job for a mid-tier engineer ($1,000/week on Cadence). Migration to S3 when you outgrow Blob is a senior-tier job ($1,500/week): rclone config, CloudFront, SDK calls, URL verification pass.
Every engineer on Cadence is AI-native, vetted on Cursor and Claude Code fluency before they unlock bookings, so they scaffold the boilerplate in an afternoon and spend the rest of the time on the parts that need a human (IAM policy review, upload UX edge cases). For deeper architecture context, see our best file upload services breakdown covering Uploadthing, Mux, and Cloudinary.
Start with the cheapest test:
If the numbers stay sane, you've avoided a week of S3 setup. If they climb fast, you have a real data point to justify the migration.
If you want a senior engineer to wire this up correctly the first time (token hygiene, signed-URL auth, alerting, sensible cache headers), audit your stack on Ship or Skip and book the work directly from the report. 48-hour free trial, weekly billing, replace the engineer any week.
Up to 5 TB per object (the underlying R2 limit). Server-side put() calls are capped at 4.5 MB by Vercel's function body limit, so anything larger has to use the client-upload path via @vercel/blob/client.
No. As of 2026, Vercel does not sign Business Associate Agreements for the Blob product. If you're handling Protected Health Information, use S3 with a signed AWS BAA, or build on a HIPAA-eligible alternative like Aptible.
Blob is a managed product wrapper on R2 storage. R2 gives you raw S3-compatible API access at $0.015/GB-month with zero egress fees, but you handle CORS, signed URLs, and CDN config yourself. Blob is roughly 10x the storage price in exchange for a one-line SDK and Vercel-native integration.
Yes. Set access: 'public' is the only documented option for the put() call, but you can keep the URL secret by not exposing it client-side, and you can generate short-lived signed URLs via the SDK for genuinely sensitive content. For strict per-request authorization, S3 with signed cookies remains the better fit.
Use rclone with the R2-compatible endpoint to mirror your bucket to S3, then update the SDK calls in your app and switch the asset URLs. For a 200 GB bucket, the data copy takes a few hours and the code change is roughly a half-day for a senior engineer. The hardest part is updating any URLs already stored in your database.
Fullstack developer at withRemote. Ships across the stack — TypeScript, Node, Postgres, Vercel. Writes on shipping speed and pragmatic architecture.