A Vercel deployment shows Building and just sits there — 15 minutes, no new log lines, no error. “Stuck building” is a different failure mode from “build failed.” Failed throws an explicit error and exits; stuck means the process is alive but making no progress. Vercel’s default build timeout is 45 minutes (Hobby) or 60 minutes (Pro) before it’s force-killed. Before that, what you’re seeing is usually a deadlocked npm install, static generation hanging on one page, a postbuild hook calling an API that never returns, or a single worker SSG-ing thousands of pages serially. This guide gives a hit-rate-ordered diagnosis with concrete timeout config.
Common causes
Ordered by hit rate, highest first.
1. Static generation stuck on one page
getStaticProps / Astro’s getStaticPaths calls an external API without a timeout, the upstream hangs. Next.js / Astro process pages serially by default — one stalls, everything after it waits. The log’s last line is usually Generating static pages (XX/YY) and then nothing.
Linting and checking validity of types ...
Collecting page data ...
Generating static pages (847/3214) ...
[stuck here for 30 minutes]
How to spot it: Generating static pages (X/Y) doesn’t increment for > 5 minutes → almost certain.
2. npm install network deadlock
npm install tries to pull a package whose registry is unreachable (private registry, blocked CDN, 404 git URL) and retries until the build timeout. Log stops at Installing dependencies... with nothing after.
npm warn deprecated ...
[no new lines for many minutes]
How to spot it: last log segment is install-phase; you never see Build completed in installation step or the framework name appear.
3. Build OOM — memory pegged but no crash
Not the instant exited with 137. Instead, heap pressure hits 99% and GC runs constantly; the process doesn’t die but each step slows to near-zero. Common in large TS projects with heavy type inference, webpack chunk splitting, or Turborepo parallel tasks.
How to spot it: gaps between log timestamps get longer (hundreds of lines in the first 5 minutes, then 1-2 per minute) — that’s GC thrashing.
4. Postbuild hook calling external service hangs
package.json has "postbuild": "node scripts/notify-cdn.js"; the script fetches a webhook without a timeout, or sitemap-generator crawls the site and stalls on a slow URL.
$ next build && next-sitemap
✓ Generating static pages (1000/1000)
$ next-sitemap
[stuck here]
How to spot it: log shows the main build finished (✓ or Build completed) but status is still Building — postbuild step is the culprit.
5. Corrupted build cache
Vercel reuses the previous build’s .next/cache or node_modules by default. If something corrupt is in there, the build can read it repeatedly and hang or error.
How to spot it: same commit builds fine locally, hangs on Vercel; the deployment’s build cache timestamp is unusually old.
6. Edge case: --watch mode in the build command
vercel.json or build command was set to next dev / vite --watch by mistake. The process never exits, building shows forever.
How to spot it: log contains ready - started server on ... or watching for changes — wrong build command.
Shortest path to fix
Step 1: Cancel manually and read where the log stopped
Dashboard → the building deployment → top-right ... → Cancel Build. After cancellation the last 50-100 lines of output remain — gold for diagnosis.
Copy the tail:
vercel logs <deployment-url> > stuck.log
tail -50 stuck.log
Match against the “Common causes” above to identify the phase.
Step 2: Stuck in static generation — add timeouts to external calls
If the log stalled at Generating static pages (X/Y), grep every untimed external call in getStaticProps / getStaticPaths / fetch(:
// Bad: no timeout
const data = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json());
// Fixed: 8-second timeout with fallback
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const data = await fetch(url, { signal: controller.signal }).then(r => r.json());
return { props: { data } };
} catch (e) {
console.warn(`Skipping ${id}: ${e.message}`);
return { props: { data: null } }; // don't block the build
} finally {
clearTimeout(timeout);
}
Step 3: Thousands of pages — switch to ISR / On-Demand
Don’t pre-generate thousands of static pages at build time. Next.js:
// pages/[slug].tsx
export async function getStaticPaths() {
return {
paths: [], // pre-build nothing
fallback: 'blocking', // generate on first request, then cache
};
}
export async function getStaticProps({ params }) {
return {
props: { ... },
revalidate: 3600, // background revalidate every hour
};
}
Astro: use output: 'hybrid' + prerender = false on dynamic routes.
Step 4: Raise build memory + watch GC
# package.json
{
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=8192 --trace-gc' next build"
}
}
--trace-gc prints every GC pause to stdout — you can see directly whether GC thrashing is the bottleneck. Hobby caps at 8GB; beyond that, you need Pro.
Step 5: Clear build cache and redeploy
Dashboard → next to the deployment, ... → Redeploy, then uncheck Use existing Build Cache. Forces a fresh install + build.
Or via CLI:
vercel --force
If clearing the cache fixes it, the cache was corrupt. Bust it more often by adding a version field to package.json (changes the cache key).
Prevention
- Force every fetch in
getStaticProps/getStaticPathsto useAbortSignal.timeout(8000) - Beyond ~200 pages, switch to ISR / on-demand — don’t bulk-generate at build time
- Add a build-duration alert in CI: fail when duration exceeds 1.5x the rolling average, forcing optimization
- Every postbuild script should have a timeout + explicit
process.exit(0) - Track deployment duration trends; a slow upward creep is usually early tech debt
- Manually clear the build cache periodically (monthly), to avoid corrupt files accumulating