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()needsBLOB_READ_WRITE_TOKEN. It signs client tokens with it. OIDC alone is not enough for this helper. (The newerhandleUploadPresigned()works with OIDC and verifies callbacks withBLOB_WEBHOOK_PUBLIC_KEYinstead.)
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.