A Vercel build that finished in 8 minutes last week suddenly runs for 45 minutes and gets killed with Build exceeded maximum duration of 45m. Sometimes it happens on Pro plans at 45 minutes, sometimes on Hobby plans at 45 minutes too — both share that wall clock. The local next build or astro build finishes in 4 minutes. The cause is almost never your application code getting slow overnight; it is build cache eviction, an unbounded getStaticPaths returning more pages than expected, a content-collection scan walking a runaway node_modules, or a post-build hook (sitemap, RSS, search index) that hangs on a network call.
Common causes
Ordered by what we see most often.
1. Build cache evicted, full cold install
Vercel’s build cache lives at .vercel/cache/ and persists node_modules, .next/cache, framework caches across deploys. If the project goes idle for ~7 days, the cache is evicted. The next build does a cold install + cold framework cache and can take 4-6 x longer.
How to spot it: Build log starts with Restored build cache from ... on a fast build, but the slow build shows No build cache found or Build cache miss.
2. getStaticPaths / content collection returns an unbounded set
A typo like paths: posts.flatMap(p => tags.map(t => p)) instead of mapping per tag can suddenly produce 10x-100x the expected number of pages. Each page costs a few hundred ms; 50,000 pages can eat the entire 45 minutes alone.
How to spot it: Build log shows Generating static pages (47832/250000) instead of a normal page count. Compare with last green build’s page count.
3. A post-build script hangs on a network fetch
Sitemap, RSS, OG-image generation, or search-index sync that calls an external API can hang for the full HTTP timeout (default 120 s per call) per item. Multiply by hundreds of items and you blow the budget.
How to spot it: The next build / astro build portion finishes in 5 minutes, but node scripts/build-sitemap.mjs or similar hangs in the log with no progress lines.
4. Memory pressure causing GC thrash near build end
If the build process runs out of heap and starts swapping (or hitting V8’s slow GC mode), the last 20% of pages can take 10x the time of the first 80%. Eventually OOM-kill or full timeout.
How to spot it: Page generation slows visibly per-batch in the log (e.g. first 1000 pages in 2 min, next 1000 in 8 min). Or you see FATAL ERROR: ... heap out of memory.
5. Large dependency install (puppeteer, sharp-libvips, Playwright)
A postinstall that downloads a 300 MB Chromium binary every cold build can add 5-10 minutes by itself. Combined with cold cache it tips the build over the limit.
How to spot it: Downloading Chromium ... or Downloading libvips ... in install log, taking longer than the rest of the install combined.
6. Type-checking on a monorepo doing whole-graph rebuild
tsc --noEmit across 200 packages without project references / incremental mode walks every file every time. On cold cache, this can be 10-20 minutes alone.
How to spot it: Build log has a tsc invocation that prints nothing for 5+ minutes before continuing.
7. Sourcemap / minification step on an oversized bundle
If a single client bundle has ballooned past ~50 MB (often from a dynamic import that accidentally pulled in aws-sdk v2 or mongodb), Terser / SWC minification can take 10x longer than usual.
How to spot it: Build log shows Optimizing bundle... or Minifying... stuck for many minutes. Compare bundle size with last green build.
Before you start
- Capture the full build log from the failing build — Vercel keeps it under Deployments → Build Logs.
- Note exact failure mode: hard 45-minute kill, OOM kill, or hang with no last log line?
- Compare last green build’s duration and page count to the failing build’s last logged page count.
- Confirm
vercel buildruns locally to completion (with--prodflag) so you can A/B against CI.
Information to collect
- Build start/end timestamps and the last successful log line.
next.config.js/astro.config.mjsand whether ISR / SSG / outputstaticis configured.- The page generation count from a healthy build vs. the failing one.
package.jsonscripts.buildand anypostbuildhook.- Output of
du -sh node_modules/locally (cold install size). - Whether the project uses Turbo / Nx / Lerna and what the build graph looks like.
Step-by-step fix
Ordered by ROI.
Step 1: Re-trigger with cache disabled to isolate cold vs. warm
In the Vercel dashboard: Deployments → … → Redeploy → uncheck “Use existing build cache”. If the cold rebuild also runs 45 minutes, the issue is in the build itself, not cache eviction. If it’s only the first cold build, cache eviction is your cause.
Step 2: Print page count early in the build
Add a quick assertion in your config or a pre-build script:
// scripts/check-page-count.mjs
import { glob } from "glob";
const files = await glob("src/content/**/*.{md,mdx}");
console.log(`[precheck] content files: ${files.length}`);
if (files.length > 10000) {
console.error("[precheck] page count over threshold");
process.exit(1);
}
Wire it as prebuild. Fails fast if the source set exploded.
Step 3: Pin and gate post-build scripts with timeouts
Wrap any networked post-build step:
# package.json scripts.postbuild
"postbuild": "timeout 300 node scripts/build-sitemap.mjs || echo 'sitemap step skipped'"
timeout 300 kills the step at 5 minutes. The build proceeds, sitemap regenerates next deploy. Better a stale sitemap than a killed deploy.
Step 4: Split build into Turborepo tasks with concurrency limits
If monorepo:
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"],
"cache": true
}
}
}
Then turbo run build --concurrency=4 instead of npm run build in the root. Cache hits across packages save 60-80% on the second run.
Step 5: Move heavy binaries out of the build
For Chromium / Playwright: do not download at install time on Vercel. Use @sparticuz/chromium packaged inside the function only, or run the screenshotting in a separate worker (Render, Fly, AWS Lambda layer) and have the build fetch URLs instead.
# package.json
"postinstall": "echo 'skipping chromium download in build'",
Set PUPPETEER_SKIP_DOWNLOAD=true in Vercel env vars.
Step 6: Switch SSG bulk pages to ISR or on-demand
If getStaticPaths returns 50k+ pages and most are rarely hit:
// pages/posts/[slug].tsx
export async function getStaticPaths() {
const topPosts = await fetchTopPosts(500); // pre-build only 500
return {
paths: topPosts.map(p => ({ params: { slug: p.slug } })),
fallback: "blocking", // generate the rest on first request
};
}
Build time drops linearly with the pre-generated set. See Next.js ISR revalidation stuck for the runtime behavior.
Step 7: Raise Node heap and bisect the bottleneck
# vercel.json or env var
NODE_OPTIONS="--max-old-space-size=8192"
Then bisect: temporarily comment out post-build scripts and see how much time the framework build itself takes. Subtract to find which step is the killer.
Verify
- Build duration returns to within 1.5x of last known good build.
- Build log shows
Restored build cacheon the second run after the fix. - Page generation count matches expectations (no surprise 10x).
- Post-build scripts each have visible start + end markers in the log.
- A test deploy with
--force(no cache) finishes well under 45 minutes.
Long-term prevention
- Alert on build duration: any deploy > 1.5x rolling-average duration should page someone.
- Pin all
postinstallheavy downloads behind env-var gates so CI can skip them. - Print a
[build-stats] pages=N duration=Xs bundle-size=Ysummary line at end of every build so you can grep history. - Treat any build cache miss event as a known cost; schedule a weekly warm-up deploy on otherwise-idle projects.
- Adopt
turboornxwith proper output caching for monorepos > 3 packages. - Limit
getStaticPathsto top-N hot pages, fall back to ISR / on-demand for the long tail.
Common pitfalls
- Bumping the team to Enterprise to “raise the limit” — Vercel’s 45-minute wall is the same on Enterprise; this needs a real fix, not a billing change.
- Adding more parallelism to a build that is already OOM-thrashing — makes it worse.
- Caching
node_modulesaggressively but forgetting.next/cache— Next.js’s content cache is where the real speedup lives. - Disabling the cache “to get a clean build” repeatedly — every cold start is a 4-6x penalty.
- Adding a
sleep 60retry around a hanging network call instead of putting a hard timeout on it. See Vercel stuck building for related hang patterns.
FAQ
Q: I’m on Pro. Can I extend the build to 60 minutes?
No — 45 minutes is the platform cap across Hobby, Pro, and Enterprise. Builds longer than 45 minutes need to be split (monorepo splitting, ISR fallback, or a pre-build job in GitHub Actions that ships an artifact).
Q: My build cache restores but the build is still slow.
Cache restore only helps frameworks that wrote into the cache directory. Make sure .next/cache, .astro/, node_modules/.cache/ are populated by your framework and not deleted by a clean step. Also check vercel.json for buildCommand that calls rm -rf somewhere.
Q: Should I move the build to GitHub Actions and just deploy artifacts to Vercel?
For builds reliably over 30 minutes, yes. Use vercel deploy --prebuilt from a CI runner with more time and memory. See Vercel build failed for the standard Vercel-side flow.
Q: Can I see why the cache was evicted?
Not directly — Vercel does not surface cache LRU events. Heuristic: any project idle for 7+ days, any change to the build command, or any Node major version bump invalidates cache. Plan accordingly.
Tags: #Troubleshooting #Vercel #Build #timeout #CI