Skip to content

Vercel Blob: client uploads vs server uploads and the 4.5 MB limit

A 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.

Vercel Blob2 min readships at docs/solutions/vercel-blob/client-uploads-and-the-4-5-mb-body-limit.md

Tags: vercel-blob · uploads · vercel-functions · body-limit · handleUpload

Your upload works on your laptop. You deploy. A 6 MB photo fails with 413 FUNCTION_PAYLOAD_TOO_LARGE.

That is the Vercel Function request body limit: 4.5 MB. It is not configurable. Any route that receives the file itself will hit it.

Server uploads: the file goes through your function

// app/api/upload/route.ts: fine for small files, breaks at 4.5 MB
export async function POST(request: Request) {
  const form = await request.formData();
  const file = form.get("file") as File;
  const blob = await put(file.name, file, { access: "private" });
  return Response.json(blob);
}

Three costs, not one:

  • The limit. Files over 4.5 MB fail.
  • Function time. You pay for every second the function spends receiving.
  • Transfer. Server uploads incur Fast Data Transfer on the way in. Client uploads have no transfer charge.

Client uploads: the file goes straight to Blob

The browser asks your route for a short-lived client token, then sends the bytes directly to Vercel Blob. Your function handles two small JSON requests. Files up to 5 TB work.

// app/api/upload/route.ts
import { handleUpload, type HandleUploadBody } from "@vercel/blob/client";

export async function POST(request: Request) {
  const body = (await request.json()) as HandleUploadBody;
  const result = await handleUpload({
    body,
    request,
    onBeforeGenerateToken: async (pathname, clientPayload) => {
      const user = await requireUser(); // throw to refuse
      return {
        allowedContentTypes: ["image/jpeg", "image/png", "image/webp"],
        maximumSizeInBytes: 10 * 1024 * 1024,
        addRandomSuffix: false,
        allowOverwrite: false,
        validUntil: Date.now() + 15 * 60 * 1000,
        tokenPayload: JSON.stringify({ userId: user.id }),
      };
    },
    onUploadCompleted: async ({ blob, tokenPayload }) => {
      // Vercel calls this after the upload. Not on localhost: see below.
    },
  });
  return Response.json(result);
}
"use client";
import { upload } from "@vercel/blob/client";

const blob = await upload(pathname, file, {
  access: "private",
  handleUploadUrl: "/api/upload",
  onUploadProgress: ({ percentage }) => setProgress(percentage),
});

What the token enforces

The constraints in onBeforeGenerateToken are signed into the token. Vercel Blob checks them at the store, not in the browser:

  • allowedContentTypes: other types are refused.
  • maximumSizeInBytes: bigger bodies are refused.
  • allowOverwrite: false: an existing pathname is refused.
  • validUntil: expired tokens are refused. The default is one hour. Use less.

That is why a client upload is safe even though the browser holds the file. The token is a narrow credential, not the read-write token.

Two things that catch people

  • You must authenticate in onBeforeGenerateToken. Without a check there, anyone can upload to your store. Throwing refuses the token.
  • handleUpload() needs BLOB_READ_WRITE_TOKEN. It signs client tokens with it. OIDC alone is not enough for this helper. (The newer handleUploadPresigned() works with OIDC and verifies callbacks with BLOB_WEBHOOK_PUBLIC_KEY instead.)

When a server upload is still right

When your own code makes the bytes: a generated PDF, a resized thumbnail, an export. Those never cross the body limit on the way in, because there is no way in. Use put() from the server for those, and client uploads for anything a user sends.

Large files

Past about 100 MB, pass multipart: true to upload(). The SDK splits the file into parts, uploads them in parallel and retries failed parts, instead of restarting from zero. Each part is one advanced operation on your bill.