Vercel 413 FUNCTION_PAYLOAD_TOO_LARGE on file uploads: upload straight to storage instead
Image or file uploads work locally but fail on Vercel with 413 FUNCTION_PAYLOAD_TOO_LARGE, or "Body exceeded 1 MB limit" in a Next.js Server Action. Why the 4.5 MB limit hits AI-built apps and how to upload directly to Supabase Storage or Vercel Blob.
What you are seeing
- Uploading a photo, PDF, or video works on localhost but fails on the deployed site with
413: FUNCTION_PAYLOAD_TOO_LARGE. - Small files upload fine and larger ones fail, roughly above 4.5 MB.
- A Next.js Server Action that takes a file fails with an error that the body exceeded the 1 MB limit, even on small photos.
- A route that returns a large JSON list or file fails with
500 - FUNCTION RESPONSE PAYLOAD TOO LARGE.
Why it happens
- The request body and the response body of a Vercel Function are each limited to 4.5 MB. Anything bigger is rejected with 413 before your code runs, so it cannot be fixed in the handler.
- AI tools often write uploads as "send the file to my API route, then the route saves it to storage". That proxies every byte through a function, so it fails as soon as files get big. Local dev servers do not enforce the limit, which is why it only shows up after deploy.
- Next.js Server Actions have their own default body size limit of 1 MB (the
serverActions.bodySizeLimitoption). Raising it helps for small files, but it cannot get you past Vercel's 4.5 MB limit. - Base64-encoding a file to send it in JSON makes it about a third bigger, so a 3.5 MB image can already go over.
How to fix it
1. Find the route that proxies the upload
In the browser's Network tab, the failing request is a POST to your own domain (for example /api/upload or a Server Action) with the file in the body. That is the hop to remove.
2. Supabase Storage: upload from the browser
// client: the signed-in user's session sends the file straight to Supabase
const { data, error } = await supabase.storage
.from('uploads')
.upload(`${user.id}/${file.name}`, file, { upsert: false });
3. Lock the bucket with a Storage policy
create policy "users upload to own folder"
on storage.objects for insert to authenticated
with check (
bucket_id = 'uploads'
and (storage.foldername(name))[1] = (select auth.uid()::text)
);
-- add a matching select policy if users need to read their files back
4. Or let the server sign, the browser upload
// server route: check the user, then hand out a one-time upload URL
const { data } = await supabaseAdmin.storage
.from('uploads')
.createSignedUploadUrl(`${user.id}/${fileName}`);
return Response.json({ path: data.path, token: data.token });
// client
await supabase.storage.from('uploads')
.uploadToSignedUrl(path, token, file);
5. Vercel Blob: use client uploads
// client
import { upload } from '@vercel/blob/client';
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: '/api/upload',
});
// /api/upload uses handleUpload() from '@vercel/blob/client'
// and must check the user in onBeforeGenerateToken,
// or anyone can upload to your store
6. Shrink large responses too
If the 413 or 500 is on a response, return less per request (paginate, select only needed columns), stream the response, or return a signed URL to the file instead of the file itself.
7. Tell your AI tool the rule
Add to your project rules (.cursorrules, Lovable knowledge, or Bolt prompt): "Never send uploaded files through an API route or Server Action. Upload from the browser directly to storage with the user's session or a signed upload URL, and save only the file path in the database."
How to know it is fixed
On the deployed site, upload a file larger than 5 MB: the Network tab shows the upload going to your Supabase or Blob URL, not your own domain, and it succeeds. A logged-out request cannot upload, and one user cannot write into another user's folder. Sources: Vercel Functions limitations, Vercel: How to bypass the 4.5 MB body size limit, Next.js serverActions config, Supabase Storage access control, and Supabase createSignedUploadUrl.