bodySizeLimit Alone Isn't Enough, Hits the Error Boundary
This article may contain affiliate links. Its content is not affected by advertising.
In short
When a Server Action's body exceeds bodySizeLimit (1MB default), Next.js throws before the action can return {error}, hitting the global error boundary in app/error.tsx instead of an inline message.
Conclusion
When a Next.js Server Action’s request body exceeds serverActions.bodySizeLimit (1MB by default), the framework layer throws before the action itself can return {error}, sending it to the global error boundary in app/error.tsx. Simply raising bodySizeLimit doesn’t fix this — the same failure recurs the instant a submission exceeds the new limit. You need a total-size guard, set below bodySizeLimit, on both the client and the server, so an overage gets handled as an inline {error} instead.
Symptom
An admin screen for sharing distribution materials uploads PDFs and slide decks through a Server Action (uploadShareFile). Uploading a file over 1MB didn’t produce the inline error the form was built to show — it showed the app’s generic, app-wide error screen instead.
export default function Error({
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4 text-center">
<h1 className="text-xl font-bold text-gray-900">
Something went wrong
</h1>
<p className="mt-2 text-sm text-gray-500">
Please try again later. Contact the administrator if the problem persists.
</p>
<button onClick={reset} ...>Reload</button>
</main>
);
}
This component (app/error.tsx) is the app-wide error boundary — it wasn’t written for any one form. Yet simply picking a file over 1MB was enough to land there.
Cause
The uploadShareFile Server Action was designed to return a value like {error: "..."} internally, with the calling form rendering that value inline. For a validation error, that’s exactly the path it should have taken — never reaching the generic error screen.
The actual cause was Next.js’s default bodySizeLimit for Server Actions (1MB). Distribution materials like PDFs and slide decks routinely exceed 1MB, and once the request body exceeds that limit, the Next.js framework layer throws an exception before the action’s own processing even finishes. The action’s code is trying to return {error}, but the framework intercepts and throws before execution gets there — so the caller receives a thrown exception instead of “the value the action returned,” and React’s error.tsx boundary catches it and shows the generic screen.
experimental: {
serverActions: {
// Server Actions request bodies default to a 1MB cap. Distribution materials
// (PDFs, slides, etc.) routinely exceed 1MB, and at the default limit uploads
// to /admin/share get rejected at the framework layer — the Server Action
// throws before it can return, so the global error boundary (app/error.tsx)
// fires (= the share button shows an error screen).
bodySizeLimit: "4.5mb",
},
},
Simply raising bodySizeLimit isn’t a real fix. Raise it to 4.5MB and the same thing happens again the moment a submission exceeds 4.5MB. What’s needed is aligning “what the action’s code can handle” with “what the framework accepts as a body,” and catching an overage on the app side before it ever reaches the framework’s own limit, so it’s handled as {error}.
The fix
bodySizeLimit was first set to 4.5MB. That’s not an arbitrary number — it matches Vercel’s cap on Function request bodies (anything larger gets rejected with 413).
On top of that, an app-level total-size limit was defined as a single source of truth, set below bodySizeLimit.
/**
* The total combined size allowed for one submission of a distribution-file
* upload. Both the client (ShareFileUploadForm) and the server
* (uploadShareFile/createBundle) reference this single source and reject at
* the same line.
*
* Set 512KB below next.config.ts's serverActions.bodySizeLimit (4.5mb) to
* leave headroom for multipart overhead.
*/
export const MAX_TOTAL_UPLOAD_BYTES = 4 * 1024 * 1024;
Setting the app’s own limit right at bodySizeLimit’s 4.5MB would risk exceeding it from multipart overhead alone, so the app-level limit is set 512KB lower, at 4MB. Both the client and server reference this same 4MB value from one place.
On the client (the form), the total size is computed before submission, and the submission itself is blocked if it’s over the limit.
const totalBytes = picked.reduce((s, p) => s + p.file.size, 0);
const overLimit = totalBytes > MAX_TOTAL_UPLOAD_BYTES;
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (picked.length === 0 || pending) return;
// Don't submit at all when the total exceeds the limit — submitting anyway
// would make the Server Action throw before it can return, landing on the
// global error boundary (= the share button shows an error screen).
if (overLimit) return;
const fd = new FormData();
for (const p of picked) fd.append("file", p.file);
// ...
dispatch(fd);
}
The submit button’s disabled state also incorporates overLimit, the file-list’s total-size display turns red on overage, and a persistent warning text states the limit explicitly. Together, these mean an over-limit submission never even leaves the browser.
But a client-side guard alone leaves any direct call that bypasses the form unprotected. The same 4MB total-size guard was added to the server-side actions.ts as well, returning {error} inline on overage — a second layer of defense. Neither path, client or server, ever reaches bodySizeLimit (4.5MB) itself anymore, so the framework layer’s throw never fires, and an overage is always handled as an inline {error}.
Preventing a repeat
What made this hard to see was that two different things — the Server Action’s {error} return (a normal validation-error path) and a bodySizeLimit overage (a framework-layer exception) — look identical on the surface: both “produce an error.” In reality the component branches differently: the latter never goes through the inline {error} display and instead lands on app/error.tsx. Isolating the cause came from knowing the action’s own {error} display is inline by design, and reasoning from there: “the generic error screen appeared, so something happened before the action’s own return.” Tests added for this cover two cases — a single file over the limit and multiple files whose combined size is over the limit — confirming {error} is returned and that neither Storage nor the database receives a write in either case.
Frequently asked questions
Q1What's missing if I just raise bodySizeLimit?
Raising the limit still leaves open the possibility that a request exceeds the new limit. Once it does, the framework throws before the action's own {error} return runs — so you also need a total-size guard, set below bodySizeLimit, on both the client and the server to catch it first.
Q2How high can bodySizeLimit go when deploying to Vercel?
Vercel caps Function request bodies at 4.5MB; anything larger gets rejected with 413. Setting bodySizeLimit above 4.5MB is pointless — in practice you want it at or below 4.5MB, with your own app-level total-size guard set even lower than that.
Q3How was hitting the error boundary identified as the cause?
The Server Action's {error} return was designed to render inline in the component. So when the app-wide app/error.tsx screen showed up instead, that pointed to the framework layer throwing before the action's own return — ahead of the {error} path entirely.
Environment verified
- Next.js 16.2.7 / React 19.2.4
- Found in production and fixed same day, 2026-07-22
What this article is based on
- TypeScript file lines 1-25commit d5077aa
- TypeScript file lines 79-100commit d5077aa
- TypeScript file lines 1-14commit d5077aa
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.