Next.js "Body exceeded 1 MB limit": the safe fix
Error: Body exceeded 1 MB limit. means a request to a Server Action carried more than 1 MB, the default cap Next.js puts on every action's body. Usually it's a file in a <form action={...}>. You can raise the cap with experimental.serverActions.bodySizeLimit in next.config, but keep it close to what you really need (say '5mb'), because it applies to every action in your app. For anything bigger, upload the file straight to storage with a signed upload URL and send only the path through the action.
The full message, as Next.js emits it:
Error: Body exceeded 1 MB limit.
To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit
Where the limit comes from
The check lives in Next.js's Server Action handler, action-handler.ts. Before your action runs, Next.js counts the request body as it streams in. When the count passes the limit, it throws ApiError(413, ...) with the message above and never calls your function. The message echoes your configured value, so after raising the limit to '5mb' an oversized upload reads Body exceeded 5mb limit.
The status your browser sees isn't always 413. For actions called from the client, a TODO in that same file says the handler still answers 500 and hands the error to the client, where your action call rejects with it. If your logs show 413 together with FUNCTION_PAYLOAD_TOO_LARGE, that's a different limit, covered below.
The Next.js docs say why the default is low: the 1MB cap exists "to prevent the consumption of excessive server resources in parsing large amounts of data, as well as potential DDoS attacks." Keep that sentence in mind when you raise it.
How do I increase the Server Actions body size limit?
Set bodySizeLimit under experimental.serverActions in next.config, but only as high as your largest real upload, because one value covers every Server Action in the app:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
serverActions: {
bodySizeLimit: "5mb",
},
},
};
export default nextConfig;
It takes a byte count or any string the bytes package parses: 1000, '500kb', '3mb'. Three things trip people up:
- The limit counts the raw request. Multipart boundaries and part headers are included, and the docs suggest 10 to 20 KB of headroom. A file sent as a base64 data URL instead of a
Filegrows by about a third. - There's no per-action setting. The handler reads one
serverActions.bodySizeLimitfrom your config. You can't giveuploadAvatar5 MB and keepupdateEmailat 1 MB. - It only covers Server Actions. Route Handlers don't go through this check. They have no equivalent cap from Next.js, so you set your own.
Why bodySizeLimit: '1000mb' is the wrong fix
The top answers for this error raise the limit to '50mb', '500mb' or '1000mb' and move on. It works on localhost. It also changes what every action in your app accepts.
A Server Action is a public POST endpoint. Its ID ships in your client JavaScript, and anyone can call it with a script, without your UI. Next.js reads and parses the body before your code runs, so an auth check at the top of the action doesn't stop a huge request from being received and parsed first. With a 1000 MB limit, every action becomes a way to make your server do a lot of work for an anonymous caller. Raise the limit for one avatar form and your "rename project" action takes 1000 MB too.
Next.js has shipped its own DoS fixes in this code path. GHSA-p3m5-59mp-jv5r (high severity, fixed in 15.5.28 and 16.4.1) let crafted requests to any app with a Server Action tie up the CPU. The body limit doesn't help there, and upgrading does: check the patched versions here.
There's also a practical problem. On Vercel, a function request body can't be larger than 4.5 MB. Anything bigger gets 413 FUNCTION_PAYLOAD_TOO_LARGE from the platform before Next.js sees it. So '1000mb' passes locally, then fails in production with a different error. Other hosts and reverse proxies have caps of their own (nginx's client_max_body_size, for example). And if your app has a proxy.ts, Next.js buffers request bodies for it only up to 10 MB by default and passes the rest of the request on truncated, with a warning in the log instead of an error.
The upload action that usually comes with it
The limit gets raised most often for a profile photo. Here's a common version: an avatar form, an admin Supabase client so the write "just works", and a limit raised to fit phone photos:
"use server";
import { revalidatePath } from "next/cache";
import { createAdminClient } from "@/lib/supabase/admin";
export async function uploadAvatar(formData: FormData) {
const userId = formData.get("userId") as string;
const file = formData.get("avatar") as File;
const supabase = createAdminClient();
const path = `${userId}/avatar.png`;
await supabase.storage
.from("avatars")
.upload(path, file, { upsert: true });
await supabase
.from("profiles")
.update({ avatar_url: path })
.eq("id", userId);
revalidatePath("/settings");
}
- Warningapp/settings/actions.ts:21
Server Action without auth check
At the top of every Server Action, resolve and verify the current user (e.g. const { user } = await getUser(); if (!user) throw …) and authorize the specific operation before mutating data. If you do guard it with a custom helper, this warning is a false positive.
To be clear about what GuardLayer flags: it has no rule for a large bodySizeLimit, and the setting in next.config won't show up in a scan. The finding is nextjs/server-action-no-auth. The action writes to the database and nothing in it checks who's calling. That's the part that turns a big limit into a real problem.
Look at what this action does for an anonymous caller. It takes userId from the form, so anyone can POST any user's ID and replace their avatar. It uses a service-role client, which skips RLS entirely, so no policy stops it. It never checks the file's type or size, so the 50 MB limit is the only cap. And it's callable without your settings page, as covered in why every Server Action needs its own auth check.
Fix 1: authenticate and validate inside the action
If your files fit comfortably under a modest limit, keep the action but make it check the caller and the file before it touches anything:
"use server";
import { revalidatePath } from "next/cache";
import { createClient } from "@/lib/supabase/server";
const MAX_BYTES = 2 * 1024 * 1024;
const TYPES = ["image/png", "image/jpeg", "image/webp"];
export async function uploadAvatar(formData: FormData) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Not signed in");
const file = formData.get("avatar");
if (!(file instanceof File)) throw new Error("No file");
if (file.size > MAX_BYTES) throw new Error("File too large");
if (!TYPES.includes(file.type)) throw new Error("Unsupported type");
const path = `${user.id}/avatar`;
const { error } = await supabase.storage
.from("avatars")
.upload(path, file, { upsert: true, contentType: file.type });
if (error) throw error;
await supabase.from("profiles").update({ avatar_url: path }).eq("id", user.id);
revalidatePath("/settings");
}
The user ID comes from the session, never from the form. The client is the cookie-based one, so storage and table policies still apply. Set bodySizeLimit a little above MAX_BYTES (here '3mb'). file.type comes from the browser, so treat it as a hint. The bucket settings in Fix 2 are the check the server enforces. If uploads then fail with a policy error, that's the bucket's RLS doing its job: here's how to write the storage policy.
Rate-limit the action too. Each call still costs you a full upload. Rate limiting a Server Action covers that.
Fix 2: upload straight to storage with a signed URL
For video, PDFs, or anything near Vercel's 4.5 MB, take the file out of the action. The server decides where the file goes, and the browser sends the bytes to Supabase Storage directly:
// app/settings/actions.ts
"use server";
import { createClient } from "@/lib/supabase/server";
export async function createAvatarUpload() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Not signed in");
const path = `${user.id}/avatar`;
const { data, error } = await supabase.storage
.from("avatars")
.createSignedUploadUrl(path, { upsert: true });
if (error) throw error;
return { path: data.path, token: data.token };
}
// in the client component
const { path, token } = await createAvatarUpload();
await supabase.storage.from("avatars").uploadToSignedUrl(path, token, file);
The action's body is now a few bytes, so the 1 MB default never comes up. The createSignedUploadUrl docs say the URL lets anyone holding it upload "without further authentication" and stays valid for 2 hours. That's why the path has to come from the session on the server and never from the client. A second small action can then save avatar_url for user.id. Size and type limits move to the bucket: Supabase lets you set fileSizeLimit and allowedMimeTypes per bucket, and Storage rejects uploads that break them. For the expiry trade-offs on the read side, see how long a Supabase signed URL should last.
A Route Handler is the third option when you need to stream the body yourself, for example to proxy it to S3. Remember that Next.js doesn't cap it, so check Content-Length, stop reading past your limit, and run the same auth check you'd put in an action.
FAQ
What does "Body exceeded 1 MB limit" mean in Next.js? A request to a Server Action carried more than the 1 MB default body limit, so Next.js rejected it before your function ran. It's almost always a file or a large base64 string sent through a form action.
Can I set bodySizeLimit for just one Server Action?
No. experimental.serverActions.bodySizeLimit is one value for the whole app. To give a single upload more room, move that upload to a signed upload URL or a Route Handler and leave the global limit low.
Does bodySizeLimit apply to Route Handlers? No. It's enforced only in the Server Action handler. Route Handlers get no equivalent limit from Next.js, so check the size yourself.
Why do I get 413 on Vercel after raising the limit?
Vercel Functions accept at most 4.5 MB per request body and return 413 FUNCTION_PAYLOAD_TOO_LARGE above that, whatever your next.config says. Upload large files directly to storage instead.
Is a large bodySizeLimit a security risk? It widens the denial-of-service surface of every Server Action, because Next.js reads the body before your auth check runs. It's worst on actions with no auth check at all. Keep the limit sized to your real files and authenticate inside each action.
Does GuardLayer detect a large bodySizeLimit?
No. It flags Server Actions that write data without an auth check (nextjs/server-action-no-auth), which is the usual companion of a raised limit. You can paste an action into the free checker to see the finding.
See which of your Supabase tables anyone can read, free
Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.
Check my Supabase projectPrefer to check code instead? Scan your repo, no signup.