Your deploy fails with A Serverless Function has exceeded the unzipped maximum size of 250 MB (Vercel), Generated Pages Functions bundle size is over the limit of 25.0 MiB (Cloudflare Pages), or The function "XXX" is larger than the 50MB limit / Unzipped size must be smaller than 262144000 bytes (Netlify). These are hard host limits — retrying the deploy will never help. The fix is always the same shape: find what’s biggest, cut or relocate it, then gate the size in CI so it can’t creep back.
Fastest fix (90% of cases): one heavyweight dependency — usually puppeteer, playwright, sharp, canvas, @ffmpeg/core, or a TensorFlow build — is being bundled into a serverless function. Run a bundle visualizer (Step 1 below), confirm the culprit, then either swap it for a serverless-friendly variant or move it out of the function. That single change clears most size-limit failures.
Current platform limits (as of June 2026)
Limits change, so confirm against each vendor’s docs before you tune a CI threshold. Verified June 2026 against the Vercel Functions limits, Cloudflare Workers limits, and Cloudflare Pages limits docs:
| Platform | What’s capped | Limit |
|---|---|---|
| Vercel Functions (Node.js) | Per-function bundle, uncompressed | 250 MB uncompressed / ~50 MB zipped (500 MB for Python) |
| Vercel Function request/response body | Per request | 4.5 MB |
| Cloudflare Workers (Free) | Worker script, after gzip | 3 MB gzipped (64 MB before compression) |
| Cloudflare Workers (Paid) | Worker script, after gzip | 10 MB gzipped (64 MB before compression) |
| Cloudflare Pages Functions | Generated _worker.js bundle | 25 MiB after compression |
| Cloudflare Pages / Workers static asset | Single file | 25 MiB |
| Cloudflare static asset count | Files per deploy | 20,000 (Free) / 100,000 (Paid) |
| Netlify Functions | Zipped / unzipped | 50 MB zipped, 250 MB unzipped (262144000 bytes) |
Two things people get wrong here. First, Vercel’s old “50 MB” figure is the zipped upload cap; the number you see in the error is the 250 MB uncompressed one (500 MB for Python). Both are AWS Lambda limits and neither is configurable. Second, Cloudflare raised the free Worker limit from 1 MB to 3 MB gzipped, and on paid plans the static-asset count went from 20,000 to 100,000 — but to actually get the 100,000 ceiling on Pages you must set the build variable PAGES_WRANGLER_MAJOR_VERSION=4. If you were working from a 2024 article, those numbers are stale.
Which bucket are you in?
Use the error string to jump straight to the cause.
| Error text contains | Most likely cause | Go to |
|---|---|---|
exceeded the unzipped maximum size of 250 MB (Vercel) | One heavy dep in a Vercel Function | Causes 1, 4 |
Unzipped size must be smaller than 262144000 bytes / larger than the 50MB limit (Netlify) | Heavy dep or whole public/ shipped into the function | Causes 1, 2 |
bundle size is over the limit of 25.0 MiB (Cloudflare Pages) | Heavy dep, source maps, or nodejs_compat off in a Pages Function | Causes 1, 5 |
Script startup exceeded CPU time limit / Worker exceeded ... size | Worker bundle too big (Free 3 MB gzipped) | Causes 1, 5 |
20,000 files / too many files (Cloudflare) | Too many static assets (dedupe, prune) | Cause 2 |
| Artifact is huge but business code is tiny | tsconfig not excluding tests/fixtures | Cause 3 |
| Server package name shows up in a client chunk | Leaky server/client boundary | Cause 4 |
Common causes
Ordered by hit rate.
1. One dependency bundles huge
puppeteer (ships Chromium, ~170 MB), playwright, canvas, sharp, @ffmpeg/core, @tensorflow/tfjs-node — native or browser-engine deps are the usual culprits. It looks like you only imported one function; in reality the entire binary lands in the bundle.
Typical Vercel error:
Error: The Serverless Function "api/screenshot" is 287 MB which exceeds
the maximum unzipped size of 250 MB.
How to spot it: du -sh node_modules/* | sort -h | tail -20 and compare the top 20 against what shipped. On Vercel, set VERCEL_ANALYZE_BUILD_OUTPUT=1 (or VERCEL_BUILDER_DEBUG=1) as a build env var to get a per-function size breakdown in the deploy logs.
2. Static assets not compressed or duplicated
Unoptimized images (4 MB JPGs), entire font families with every weight and character set (hundreds of KB per woff2), or the same image referenced from both public/ and src/assets/ so it ships twice. On Cloudflare this can also trip the file-count cap (20,000 files on Free) rather than a size cap.
How to spot it: ls -lhS dist/ | head and eyeball anything over 500 KB that probably shouldn’t be. For file count: find dist -type f | wc -l.
3. tsconfig doesn’t exclude tests / examples / dev tooling
tsconfig.json is missing exclude, so the build compiles *.test.ts, __fixtures__/, storybook/, and examples/ into the server bundle. Symptom: business code is small but the artifact is hundreds of MB.
How to spot it: after build, find dist -name "*.test.*" -o -name "*fixture*". Any hit means you missed something.
4. Server vs client bundle boundary is leaky
You imported a server-only dep (pdf-parse, @aws-sdk/client-s3) into a client component, and the bundler shipped it to the browser. Or the reverse: markdown or template strings that should live server-side get inlined into every page.
How to spot it: run the bundle analyzer and look for server-only package names in client chunks.
5. Source maps leaked into the production artifact
vite.config.ts / next.config.js defaults sometimes emit source maps in production too. Each chunk gets a .map file of roughly equal size, doubling the artifact — this is a common cause of the Cloudflare Pages 25 MiB and Worker 3 MB/10 MB overruns.
How to spot it: ls dist/assets/*.map 2>/dev/null | wc -l returns more than 0 and the total .map size is close to the total code size.
Shortest path to fix
Step 1: Find what’s biggest
Start with physical size:
npm run build
du -sh dist/
du -sh dist/* | sort -h | tail -10
Then run a bundle visualizer to see which dep dominates each chunk:
# Vite / Astro
npx vite-bundle-visualizer
# Next.js (needs @next/bundle-analyzer wired into next.config.js)
ANALYZE=true npm run build
# Webpack
npx webpack-bundle-analyzer dist/stats.json
Open the HTML report and pick the 1-3 biggest blocks. Those are what you’re going to cut.
Step 2: Swap heavy deps or push them server-only
| Heavy | Light |
|---|---|
moment (~290 KB) | date-fns (~13 KB tree-shaken) or dayjs (~7 KB) |
lodash (~70 KB) | lodash-es with named imports, or native ES methods |
axios (~30 KB) | native fetch |
puppeteer | @sparticuz/chromium + puppeteer-core (serverless-friendly) |
full chart.js | named import of just the modules you use |
If the dep is heavy and required (sharp, puppeteer), lift it out of the serverless function: move to a dedicated worker, an external service (Browserless, Cloudinary), or do the work at build time. On Cloudflare specifically, puppeteer and playwright won’t fit a Worker at all — use Cloudflare Browser Rendering instead of bundling a browser engine.
On Cloudflare Pages, before you start cutting, make sure the nodejs_compat compatibility flag is enabled (Settings -> Functions -> Compatibility flags, or compatibility_flags = ["nodejs_compat"] in wrangler.toml). It lets Cloudflare use its built-in Node-compatible APIs instead of bundling polyfills, which alone can drop a Next.js _worker.js bundle back under the 25 MiB line. Importing scoped sub-packages (@googleapis/calendar instead of the whole googleapis) is the other big win on Pages.
Step 3: Tighten tsconfig and build include / exclude
// tsconfig.json
{
"compilerOptions": { "...": "..." },
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**",
"**/__fixtures__/**",
"examples/**",
"storybook/**"
]
}
On Vercel, vercel.json lets you exclude precisely per function:
{
"functions": {
"api/**/*.ts": {
"includeFiles": "lib/**",
"excludeFiles": "{tests,fixtures}/**"
}
}
}
Note: includeFiles / excludeFiles in vercel.json are ignored by Next.js. For a Next.js app, use outputFileTracingIncludes / outputFileTracingExcludes in next.config.js instead:
// next.config.js
module.exports = {
outputFileTracingExcludes: {
"*": ["**/*.test.*", "**/__fixtures__/**", "node_modules/@swc/**"],
},
};
Step 4: Compress static assets and disable source maps
Images:
# Compress all PNG / JPG under public/
npx @squoosh/cli --mozjpeg auto public/**/*.{jpg,jpeg}
npx @squoosh/cli --oxipng auto public/**/*.png
# Or convert to WebP / AVIF
npx @squoosh/cli --webp auto public/**/*.{jpg,png}
Kill production source maps:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: false, // or 'hidden' to upload to Sentry without shipping publicly
},
});
Step 5: Gate bundle size in CI
# .github/workflows/bundle-size.yml
name: Bundle Size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci && npm run build
- name: Check size
run: |
SIZE=$(du -sm dist | cut -f1)
echo "dist size: ${SIZE} MB"
if [ "$SIZE" -gt 180 ]; then
echo "Bundle exceeds 180 MB threshold"
exit 1
fi
Set the threshold roughly 70-80% of your platform’s hard limit so you have headroom for growth (e.g. ~180 MB against Vercel’s 250 MB, ~7 MB against a Cloudflare paid Worker’s 10 MB).
How to confirm it’s fixed
- Re-run the build locally and compare:
du -sh dist/should be visibly smaller than before. - For Vercel, set
VERCEL_ANALYZE_BUILD_OUTPUT=1and check the per-function sizes in the deploy log — every function should be comfortably under250 MB. - Redeploy. A clean deploy with no size-limit error in the log is the real confirmation; the build log shows the final function/asset sizes near the end.
- Confirm the CI guard works by temporarily lowering the threshold below your current size and watching the PR check fail, then restore it.
Prevention
- Before adding any new dep, check its minified + gzipped size on bundlephobia.com; anything over
50 KBneeds a justification or a lighter alternative. - Keep the bundle-size CI check pinned at 70-80% of the platform’s hard limit.
- Separate server-only and client-only deps clearly; never import a server package from a client component.
- Push large binaries (PDFs, videos, model weights) to object storage (R2, S3, Cloudinary) and fetch at runtime instead of bundling.
- Quarterly: run
npx depcheckto remove unused deps, then rerun the bundle visualizer to revisit the top 5 heaviest deps.
FAQ
Vercel says the limit is 50 MB in some docs but my error says 250 MB. Which is it?
Both are real. AWS Lambda (which backs Vercel and Netlify Functions) caps the zipped upload at ~50 MB and the unzipped bundle at 250 MB. The number Vercel surfaces in the build error is the 250 MB unzipped figure. As of June 2026, treat 250 MB uncompressed (500 MB for Python) as the cap you need to stay under; it is enforced by AWS and not configurable. See Vercel’s function limits page.
My Cloudflare Worker was fine last year and now it’s “over the limit.” Did the limit shrink?
No — Cloudflare actually raised the free Worker limit from 1 MB to 3 MB gzipped (paid is 10 MB; both also cap at 64 MB before compression). If you’re newly over, a dependency grew or you started bundling something (often source maps or a new package). Run wrangler deploy --dry-run --outdir dist to see the compressed size before deploying, and on Pages confirm nodejs_compat is on.
The zip is only 30 MB but Netlify says it’s over 50 MB (or 262144000 bytes). Why?
Netlify measures the unzipped size against the 250 MB cap (262144000 bytes) and the zipped size against 50 MB. A 30 MB zip can unpack to well over 250 MB if it contains a browser engine or native binaries. Check the unzipped size locally before deploying.
Will deleting the deploy and retrying help? No. Size limits are deterministic — the same source produces the same oversized bundle every time. You must actually remove or relocate the weight (Steps 2-4). Retrying only wastes build minutes.
How do I keep a browser-automation dependency without blowing the limit?
Don’t bundle the browser. On Vercel use puppeteer-core + @sparticuz/chromium; on Cloudflare use Browser Rendering; or offload to a hosted service like Browserless. The function then ships only the client library, not the ~170 MB engine.