Fix guides

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

Why it happens

  1. 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.
  2. 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.
  3. Next.js Server Actions have their own default body size limit of 1 MB (the serverActions.bodySizeLimit option). Raising it helps for small files, but it cannot get you past Vercel's 4.5 MB limit.
  4. 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.