New:Upstash has a remote MCP server

Best Vercel Blob Alternatives in 2026

YusufYusuf
16 min read

The best Vercel Blob alternative for most Next.js apps is Upstash Blob. Upstash Blob has the same upload model as Vercel Blob (a server put(), an upload handler route and React hooks for browser uploads), so moving off Vercel Blob is mostly renaming. Upstash Blob storage costs $0.02/GB and egress is free up to 1 TB a month on pay-as-you-go. For media apps serving many terabytes a month, Cloudflare R2 is cheaper on egress, but you write the upload flow yourself. Use Cloudinary for image transforms and S3 if the rest of your stack is on AWS.

Why do developers leave Vercel Blob?

Most developers leave Vercel Blob because of the price: downloads cost $0.05 per GB, and every cache miss adds Fast Origin Transfer and an operation on top.

Two other reasons come up:

  • Upload callbacks don't run locally. The onUploadCompleted callback for browser uploads does not work on localhost because Vercel Blob can't reach your dev server, so Vercel suggests a tunnel like ngrok in development.
  • It is tied to Vercel hosting. The SDK prefers Vercel's rotating OIDC (OpenID Connect) tokens. Outside Vercel you can use a long-lived read-write token, but image optimization with putImage() needs OIDC.

A Vercel Blob bill has six lines. Four belong to Blob and two come from Vercel's CDN:

MeterWhat triggers itOn-demand price (iad1)
StorageMonthly average of the store size$0.023 per GB-month
Simple operationsA read by URL that misses the cache, or a metadata lookup$0.40 per 1M
Advanced operationsUpload, copy, list, or creating a store$5.00 per 1M
Blob Data TransferEvery download or view$0.05 per GB
CDN RequestsEvery access by URL, hit or missStandard CDN rates
Fast Origin TransferOnly cache missesStandard CDN rates ($0.06/GB in Vercel's own example)

Here is the pricing table on Vercel's page:

With Vercel Blob, you are charged per CDN request and data transfer for any downloads. If the object is not cached, you'll also be charged for a simple operation and Fast Origin Transfer.

If we take the example Vercel provides on their site, with 50 GB of storage, 350 GB of data transfer and a 70% cache hit rate, it'll cost $26.93 a month. Of that, $17.50 is data transfer costs.

It gets worse if you serve large files, such as videos. Vercel only caches blobs up to 512 MB. This means any file larger than 512 MB is a cache miss on every access, so you will always be charged for Fast Origin Transfer and CDN requests for every access. So, if you serve a 600 MB video, you are always paying more the more it gets accessed.

You can lower data transfer costs on Pro plans by moving transfer to Flat Rate CDN, which includes 1 TB of bandwidth. However, this isn't possible if you run a media application. On their Flat Rate CDN page, Vercel writes that "large-scale delivery of media or files where such delivery constitutes a majority of bandwidth usage" is out of scope.

Vercel Blob alternatives compared

Notes on numbers: all prices in USD, per vendor pricing page, on pay-as-you-go / lowest paid plan. Price for operations is per million requests. Writes are uploads (Class A for R2 and Tigris, PUT for S3), reads are downloads (Class B, GET).

ServiceStorage per GB-monthEgress per GBWrites / reads per 1MFree tierBest at
Vercel Blob$0.023$0.05, plus Fast Origin Transfer on misses$5.00 / $0.40 (misses only)Hobby: 1 GB, 10 GB transferStaying on Vercel
Upstash Blob$0.02Free up to 1 TB, then $0.02$4.50 / $0.301 GB, 10 GB egressNext.js uploads under 1 TB egress
Cloudflare R2$0.015Free$4.50 / $0.3610 GB, 1M writes, 10M readsEgress-heavy media
Tigris$0.02Free$5.00 / $0.505 GB, 10k writes, 100k readsZero egress, same price in every region
Backblaze B2$6.95 per TB (about $0.007)Free up to 3x stored, then $0.01FreeFirst 10 GBCheapest storage
AWS S3 Standard$0.023 (us-east-1)$0.09 (first 100 GB/month free)$5.00 / $0.40Up to $200 in credits for new accountsApps already on AWS
Supabase Storage$0.0213 after 100 GB on Pro$0.03 cached / $0.09 uncached after 250 GB eachNot listed1 GB on FreeApps on Supabase auth and Postgres
UploadThing$10/month for 100 GB, $0.08/GB past 250 GB on $25 planNot listedNot listed2 GBHosted upload flow with flat plans
Cloudinary1 credit = 1 GB1 credit = 1 GB of image bandwidth1 credit = 1,000 transforms25 credits a monthImage and video transforms

Supabase Pro is $25 a month and Cloudinary Plus is $99 a month for 225 credits, so those bills start above zero.

What the same app costs on each one

The traffic numbers for these two example apps are our assumptions, so swap in your own.

For Vercel Blob we assumed a 30% cache miss rate and used $0.06/GB as the Fast Origin Transfer rate, per Vercel’s example.

We didn’t include the base cost of paid plans, except for Supabase, UploadThing and Cloudinary, where the plan is the price.

Prices are list prices from the table.

App A: a small SaaS. 20 GB stored, 200 GB downloaded, 100,000 uploads and 2 million reads a month.

ServiceMathMonthly bill
Cloudflare R2(20 − 10 free GB) × $0.015; ops inside the free tier$0.15
Upstash Blob20 × $0.02 + 0.1M × $4.50 + 2M × $0.30; egress under 1 TB$1.45
Backblaze B2(20 − 10 free GB) × $0.00695 + (200 − 60 free GB) × $0.01$1.47
Tigris(20 − 5) × $0.02 + 90k writes × $0.005/1k + 1.9M reads × $0.0005/1k$1.70
UploadThing$10 plan covers 100 GB$10.00
AWS S320 × $0.023 + (200 − 100) × $0.09 + $0.50 PUTs + $0.80 GETs$10.76
Vercel Blob$0.46 storage + $0.24 misses + $0.50 uploads + 200 × $0.05 + 60 GB × $0.06$14.80 (2M CDN requests fit the included 10M)
Supabase StorageInside Pro's 100 GB and 250 GB cached egress$25.00 (Pro plan)
Cloudinary20 + 200 = 220 credits, fits Plus$99.00 (Plus plan)

At this size Upstash Blob, R2, B2 and Tigris all come in under $2, and the real choice is about the upload code, not the price. Vercel Blob's $14.80 is mostly the $10 of transfer.

App B: a media app. 500 GB stored, 3 TB (3,000 GB) downloaded, 500,000 uploads and 20 million reads a month.

ServiceMathMonthly bill
Cloudflare R2490 × $0.015 + (20M − 10M free) × $0.36/1M$10.95
Backblaze B2490 × $0.00695 + (3,000 − 1,500 free GB) × $0.01$18.41
Tigris495 × $0.02 + 490k × $0.005/1k + 19.9M × $0.0005/1k$22.30
UploadThing$25 plan + (500 − 250) × $0.08$45.00
Upstash Blob500 × $0.02 + (3,000 − 1,000) × $0.02 + 0.5M × $4.50 + 20M × $0.30$58.25
Supabase Storage$25 + (500 − 100) × $0.0213 + (3,000 − 250) × $0.03 cached$116.02
Vercel Blob$11.50 + $2.40 misses + $2.50 uploads + 3,000 × $0.05 + 900 GB × $0.06$220.40 + CDN requests past the included 10M
AWS S3500 × $0.023 + 2,900 × $0.09 + $2.50 PUTs + $8.00 GETs$283.00
Cloudinary500 + 3,000 = 3,500 credits, past Advanced's 600Custom plan

Past 1 TB of downloads a month, the zero-egress stores cost less than Upstash Blob. Upstash Blob still costs about a quarter of Vercel Blob for the same traffic, with the same upload code.

Here are the App B bills side by side:

Upstash Blob

Here are Upstash Blob's plans:

On pay-as-you-go, storage costs $0.02 per GB-month. Egress is free up to 1 TB per month, and then $0.02 per GB after that. Uploads and deletes do not count as bandwidth. (We wrote more about the change in pricing in Upstash Blob now includes 1 TB of free egress every month.)

Each bucket is global, so you don't choose a region for it, and you don't pay anything extra for replication.

Upstash Blob's programming model is closer to Vercel Blob than anyone else's, especially for a Next.js app. Its TypeScript SDK has a server put(), an upload handler that corresponds to handleUpload, and React hooks to use in the browser. Uploads do not go through the 4.5 MB request body limit on Vercel functions because the browser sends files directly to storage.

The upload handler calls a completion callback. With Upstash Blob, the callback runs in your own route, and the browser calls the route to complete the upload. When we tested the browser-side upload against a route on a local Node server, the callback ran without setting up a tunnel or a public URL.

Upstash also lets you swap the bucket token for temporary S3 credentials with bucket.s3(), so you can use the AWS SDK, the AWS CLI, and other S3-compatible tools on the same bucket. If you already use Upstash Redis or QStash, then Upstash Blob is also one more product on the same account.

For an app like App A, Upstash Blob is the cheapest of the stores that ship a Next.js upload flow. Our guide to file storage for Next.js goes deeper on that setup.

Cloudflare R2

For media apps whose downloads go into terabytes, Cloudflare R2 has the lowest egress cost in this list. For App B above, 3 TB downloads cost $10.95 on R2, and $220.40 on Vercel Blob.

Both storage classes offer free egress. R2's pricing table is:

R2 charges nothing for egress on any storage class. Storage is $0.015 per GB-month, and the free tier is 10 GB, 1 million writes, and 10 million reads every month.

The tradeoff is you need to do more work to set uploads up: there is no Next.js upload handler, so you generate presigned URLs with an S3 client and write the upload route yourself.

With egress free, reads dominate the bill at high traffic. Cloudflare's own example of 100,000 small files read 10 million times a day costs $104.40 a month on R2, all of it in Class B reads.

Choose R2 for: Video, podcasts, game assets, public datasets, or any app where download traffic dominates the bill.

UploadThing

UploadThing offers a hosted upload flow, as well as a range of flat plans: free up to 2 GB of storage, with unlimited uploads and downloads; $10/month for up to 100 GB; and a $25 usage plan, which includes 250 GB of storage, with each additional GB of storage costing $0.08.

Because UploadThing prices based on storage, and not downloads, it's attractive for apps with small storage requirements but larger volumes of downloads. In App B, UploadThing worked out to $45/month for 3 TB of downloads.

The tradeoff is price of per-GB storage, which is the highest of the plain file stores we looked at. For stores with more than 250 GB, UploadThing's $0.08 per-GB storage price makes it 4x the price of storage on Upstash Blob or Tigris for 1 TB of files, and the $10 plan is limited to 100 GB.

Supabase Storage

Your Supabase Storage files are located in the same project as your database rows. If your application already uses Supabase for Postgres and auth, Supabase Storage is a good fit.

Supabase Storage also comes with image transforms: the Pro plan includes 100 origin images, then $5 per 1,000.

Supabase’s pricing page says this about storage:

Pro ($25 per month) gets 100 GB storage and 250 GB cached egress. Overage is $0.0213 per GB storage and $0.03 per GB cached egress.

But watch out for the uncached egress line item: downloads that miss Supabase’s CDN incur a higher $0.09 per GB overage past the 250 GB included. That’s nearly twice as expensive as Vercel Blob’s transfer rate. If you have a media application with a long tail of files, you’ll find more downloads on that uncached line item.

AWS S3

S3 costs the most in this list for public downloads. They charge egress costs for downloads to the internet. It starts at $0.09 per GB for the first 10 TB after a free 100 GB a month shared across AWS services. This means App B is more expensive with S3 ($283.00) than it is with Vercel Blob.

This can be worked around by placing CloudFront in front of S3, as data transfer from S3 to CloudFront is free, allowing you to use CloudFront’s rates instead. This means more services to set up and bill. For a more detailed look at cost, check out our S3 vs Upstash Blob comparison.

AWS has the most advanced ecosystem with IAM policies, storage classes for cold data, and support for objects up to 50 TB. S3 is a sensible option when the rest of your app is on AWS. New AWS accounts can receive up to $200 in Free Tier credits that can be used within 12 months.

Tigris

Tigris is globally distributed, S3-compatible object storage with zero egress fees. It costs $0.02 per GB-month in any region, and defaults to global placement, meaning Tigris moves data closer to where it's being read.

Tigris costs more per operation than R2: $5.00 per million writes and $0.50 per million reads. Its free tier is also lower, offering 5 GB, 10,000 writes and 100,000 reads per month. Like R2, Tigris has no Next.js upload handler, so you'll be building presigned uploads with an S3 client.

Tigris is a good fit for apps serving users globally and want R2-style free egress without a fixed region. Tigris costs $22.30 per month in App B.

Cloudinary

Cloudinary is file storage specifically for images and video. Cloudinary offers resizing, cropping, format conversion, and transcoding on-the-fly. Supabase Storage also resizes images, but none of the other stores here transform files, and Vercel Blob's putImage() needs Vercel's OIDC auth. Cloudinary is the only one in this list that also handles video.

Cloudinary charges using "credits", with each credit equalling 1,000 transformations, 1 GB of storage, or 1 GB of image bandwidth. The free tier includes 25 credits per month, Plus is $99/mo for 225, Advanced is $249 for 600.

Cloudinary is very expensive for just file storage. Storing App A's 20 GB of files and 200 GB of downloads would take 220 credits, bumping you up to the $99/mo plan. App B's 3,500 credits exceeds Advanced. Cloudinary also requires Advanced for custom domains (CNAME). Use Cloudinary when you need its file transformation features.

Backblaze B2

B2 is the most inexpensive option on the list, at $6.95 per TB per month, which is about $0.007 per GB. The first 10 GB of storage is free, and API calls are free on pay-as-you-go pricing.

This is the pay-as-you-go card:

The egress is free up to 3 times your average amount stored, after which it’s $0.01 per GB. Egress through any of its partner CDNs, including Cloudflare, Fastly and bunny.net is free even after that point, though.

Like R2 and Tigris, there’s no Next.js upload flow.

The free egress on B2 is dependent on the amount of storage. This means it’ll cost more if you have a small storage size with more traffic. In app B, the storage is 500 GB, which means only 1,500 GB of the 3 TB of downloads are free.

The pricing makes B2 a great choice if you have any big archives, backups, or user libraries where you’ll store a lot of data and download a share of it.

Migrating from @vercel/blob to @upstash/blob

Most of the move is renaming. The two SDKs map like this:

Vercel Blob (@vercel/blob)Upstash Blob (@upstash/blob)
BLOB_READ_WRITE_TOKENUPSTASH_BLOB_TOKEN, read by Bucket.fromEnv()
put(path, body, { access })bucket.put(path, body); public or private is a bucket setting
addRandomSuffix: trueuniquePath(name) or allowOverwrite: false
handleUpload() in a POST routeuploadHandler(), exported as GET and POST
onBeforeGenerateTokenonBeforeUpload
onUploadCompleted({ blob })onUploadComplete({ path, url, size, ... })
upload() from @vercel/blob/clientuseUpload() from @upstash/blob/react

Server upload

This route takes a file from a form post and stores it. On Vercel Blob:

import { put } from '@vercel/blob'
 
export async function POST(request: Request) {
  const form = await request.formData()
  const file = form.get('file')
  if (!(file instanceof File)) {
    return Response.json({ error: 'A file is required' }, { status: 400 })
  }
 
  const blob = await put(file.name, file, {
    access: 'public',
    addRandomSuffix: true,
  })
  return Response.json(blob)
}

The same route on Upstash Blob:

import { Bucket } from '@upstash/blob'
 
const bucket = Bucket.fromEnv()
 
export async function POST(request: Request) {
  const form = await request.formData()
  const file = form.get('file')
  if (!(file instanceof File)) {
    return Response.json({ error: 'A file is required' }, { status: 400 })
  }
 
  const blob = await bucket.put(file.name, file, { allowOverwrite: false })
  return Response.json(blob)
}

We ran the Upstash route against a real bucket with a small text file. It returned:

{
  "path": "article-test-1793617187-route/route.txt",
  "url": "https://b27353de2921.blob.upstash.io/article-test-1793617187-route/route.txt",
  "versionedUrl": "https://b27353de2921.blob.upstash.io/article-test-1793617187-route/route.txt?v=%22007d1cfeac55615ca753010402965b23%22",
  "size": 20,
  "etag": "\"007d1cfeac55615ca753010402965b23\"",
  "uploadedAt": "2026-11-02T10:59:48.337Z",
  "contentType": "text/plain"
}

Note that the uploaded File provides the content type. For a simple string where the content type is missing, the content type application/octet-stream is used.

Browser upload

For files bigger than a function body allows, the browser uploads directly. The Vercel version has a token route and a client component:

import {
  handleUpload,
  type HandleUploadBody,
  type HandleUploadOptions,
} from '@vercel/blob/client'
 
async function onBeforeGenerateToken() {
  // Check the user's session and permission to upload here.
  return {
    allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp'],
    maximumSizeInBytes: 5_000_000,
    addRandomSuffix: true,
  }
}
 
async function onUploadCompleted({ blob }: Parameters<
  NonNullable<HandleUploadOptions['onUploadCompleted']>
>[0]) {
  console.log('Upload completed:', blob)
}
 
export async function POST(request: Request) {
  const body = (await request.json()) as HandleUploadBody
  const result = await handleUpload({
    request,
    body,
    onBeforeGenerateToken,
    onUploadCompleted,
  })
  return Response.json(result)
}
'use client'
 
import { upload } from '@vercel/blob/client'
import type { ChangeEvent } from 'react'
 
async function uploadAvatar(event: ChangeEvent<HTMLInputElement>) {
  const file = event.currentTarget.files?.[0]
  if (!file) return
 
  const blob = await upload(file.name, file, {
    access: 'public',
    handleUploadUrl: '/api/avatar/upload',
  })
  console.log('Uploaded:', blob)
}
 
export default function AvatarUpload() {
  return (
    <input
      type="file"
      accept="image/jpeg,image/png,image/webp"
      onChange={uploadAvatar}
    />
  )
}

Here's how this looks on Upstash Blob, where the browser requests permission from your route, then sends the bytes directly up to the bucket, then notifies your route of completion:

The route in app/api/upload/route.ts declares the limits once and exports the handler:

import {
  uniquePath,
  uploadHandler,
  type BeforeUploadArgs,
  type UploadCompleteArgs,
} from '@upstash/blob'
 
function onBeforeUpload({ file }: BeforeUploadArgs) {
  // Check the user's session and permission to upload here.
  return { path: uniquePath(file.name) }
}
 
function onUploadComplete({ path, url, size, contentType }: UploadCompleteArgs) {
  console.log('Upload completed:', { path, url, size, contentType })
  return { path }
}
 
const uploads = uploadHandler({
  constraints: { maxSize: '5mb', contentTypes: ['image/*'] },
  onBeforeUpload,
  onUploadComplete,
})
 
export type Uploads = typeof uploads
 
export const { GET, POST } = uploads

The client component reads those limits from the route, so the file picker's accept value and the size check stay in sync with the server:

'use client'
 
import { uploadHooks } from '@upstash/blob/react'
import type { ChangeEvent } from 'react'
import type { Uploads } from './api/upload/route'
 
const { useUpload } = uploadHooks<Uploads>()
 
type StartUpload = ReturnType<typeof useUpload>['start']
 
function uploadAvatar(start: StartUpload, event: ChangeEvent<HTMLInputElement>) {
  start({ file: event.currentTarget.files?.[0] })
}
 
export default function AvatarUpload() {
  const { start, upload, accept } = useUpload()
 
  return (
    <div>
      <input
        type="file"
        accept={accept}
        onChange={uploadAvatar.bind(null, start)}
      />
      {upload?.pending && <progress value={upload.percent} max={100} />}
      {upload?.status === 'done' && <p>Uploaded: {upload.blob.data.path}</p>}
      {upload?.status === 'error' && <p>{upload.error.message}</p>}
    </div>
  )
}

We uploaded a small PNG from Chromium to this route running on a local Node server. The route logged both phases, and the callback ran on the local machine:

handler phase: begin
handler phase: end
Upload completed: {
  path: 'article-test-1793617201-browser/avatar-RDtvZwGx.png',
  url: 'https://b27353de2921.blob.upstash.io/article-test-1793617201-browser/avatar-RDtvZwGx.png',
  size: 68,
  contentType: 'image/png'
}
public URL: HTTP 200, 68 bytes

In between those phases, the browser had uploaded the file bytes straight to storage. Whatever onUploadComplete returned, the browser got back under data, which is how the component displayed upload.blob.data.path. Finally we cleaned up the test objects by prefix and listed the prefix to make sure it was empty.

One thing to watch out for as you port this is that onUploadComplete can run more than once if the browser retries its finishing request after a network error, so you want to upsert your database row on the upload id rather than always inserting.

Which Vercel Blob alternative should you pick?

For most Next.js apps leaving Vercel Blob, the best alternative is Upstash Blob. The exceptions depend on how much you serve:

  • If you have a Next.js app, upload from the browser, and serve under 1 TB of downloads a month: Upstash Blob. You can port your upload code almost line for line, and you get App A for $1.45 a month.
  • If you serve video or other large public files, many terabytes a month: Cloudflare R2. App B costs $10.95, but you write the upload route yourself.
  • If you want free egress and users worldwide without choosing a region: Tigris.
  • If you host large archives or backups with modest downloads: Backblaze B2.
  • If you host images and want on-the-fly resizing or format changes: Cloudinary, or Supabase Storage if you already use Supabase.
  • If you already use Supabase for auth and Postgres: Supabase Storage.
  • If you want a hosted upload flow, flat plans, and low storage: UploadThing.
  • If your other systems are already on AWS: S3, with CloudFront in front for public downloads.

We have a separate guide to cloud storage APIs for side projects.