Cloud

Fix: Vercel build failed with JavaScript heap out of memory

Vercel builds crash with FATAL ERROR: Reached heap limit. Here are the real causes and the exact configuration changes that fix them.

A Vercel deployment fails with a wall of error output ending in:

<--- Last few GCs --->
[38:0x7f8c9a008d20]   152845 ms: Mark-Compact 4045.7 (4132.5) -> 4041.9 (4136.8) MB
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

Local builds work fine, but Vercel’s build container runs out of memory before finishing. Here’s why, and the exact fixes that work.

Understand the constraint

Vercel’s Hobby plan gives build containers 8192 MB of RAM. Node.js’s default V8 heap ceiling is much lower than that (roughly 2 GB on most systems, sometimes auto-scaled higher on modern Node versions but rarely more than half the container’s memory). Most heap-out-of-memory errors on Vercel are actually V8 hitting its own default, not the container limit.

Pro plan raises the container to 16384 MB and lets builds run longer, but V8’s default is still 4096 MB unless you raise it explicitly.

Diagnose — is it V8, the container, or a runaway build?

Check the build log for:

  • Reached heap limit — V8’s default heap ceiling hit (typically ~2 GB on 64-bit systems unless overridden). Solvable with a config change.
  • Killed or Exit code 137 — container OOM killed. Vercel container ran out.
  • Build exceeded maximum duration of 45 minutes — different problem, memory is fine but build is slow.

The rest of this article focuses on the first two.

Cause 1 — V8 heap default too low (most common)

Node’s build tool (webpack, esbuild via a plugin, TypeScript compiler) tried to allocate more than V8’s default heap ceiling and V8 said no.

Fix — raise V8’s heap ceiling via NODE_OPTIONS:

In Vercel dashboard → Project → Settings → Environment Variables, add:

Name:  NODE_OPTIONS
Value: --max-old-space-size=7168

Apply to Production and Preview. Redeploy.

For Hobby plan, 7168 leaves ~1 GB of headroom inside the 8 GB container (Vercel’s own recommendation). Setting to 8192 uses the full container and can trigger OS-level OOM kills. For Pro plan with Elastic build machines, safe values scale up — start with 12288 if your build needs more.

Alternative — set it only for the build command in package.json:

{
  "scripts": {
    "build": "NODE_OPTIONS='--max-old-space-size=7168' next build"
  }
}

Environment variable is preferred — no code change, applies to every framework.

Cause 2 — Too many pages being statically generated at once

Static site generators (Next.js SSG, Astro, Gatsby, Remix) hold all page data in memory during build. A blog with 500 markdown files can push memory past 4 GB even with the fix above.

Diagnose: Check the build log for a line like:

Generating static pages (0/1247)

If the number is large and the OOM happens partway through, this is your cause.

Fix — batch or defer generation:

Next.js:

// next.config.js
module.exports = {
  experimental: {
    workerThreads: false,
    cpus: 1,  // serialize page generation
  },
};

Astro — switch to on-demand rendering for rarely-visited pages:

// src/pages/archive/[slug].astro
export const prerender = false;

This moves generation from build time to request time. First hit is slower; build succeeds.

Cause 3 — Image processing at build

Frameworks that process images at build (Astro <Image>, Next.js <Image> with static import, Gatsby gatsby-plugin-sharp) load full-size sources into memory. A single 20 MB PNG can spike 200 MB heap during processing.

Fix — pre-optimize images before commit:

Google’s @squoosh/cli was deprecated in 2023 (CLI + libsquoosh support removed from the Squoosh project that year). Current options:

Sharp (recommended, actively maintained):

npx sharp -i "public/*.png" -o public/out -f webp --width 1600

Or the community-maintained Squoosh fork:

npx @sbcinnovation/squoosh-cli --webp auto public/screenshots/*.png

The Squoosh web app (squoosh.app) is still maintained for browser-based one-off optimization.

Commit the smaller files. Build heap drops immediately.

Alternative — use Vercel’s image CDN instead of build-time processing:

Next.js <Image> with unoptimized: false uses Vercel’s on-request image API. No build memory cost. Free on Hobby up to 5,000 image transformations/month (plus 300K cache reads and 100K cache writes) — verify against Vercel Image Optimization pricing.

Cause 4 — Bundling a giant client dependency

Some libraries include their entire ecosystem in the client bundle. Common offenders: moment (72 KB gzipped alone, more with locales), lodash (full import), aws-sdk v2, three.js.

Diagnose — analyze the bundle:

Next.js:

ANALYZE=true npm run build

(Requires @next/bundle-analyzer in next.config.js.)

Astro:

npm run build -- --verbose

Look for individual chunks > 500 KB.

Fixes:

  • Replace moment with date-fns (tree-shakeable, uses ~10x less RAM to bundle)
  • Import lodash by function, not the root: import debounce from 'lodash/debounce'
  • Move heavy dependencies to server-only routes (Next.js app/ server components, Astro’s default server rendering)

Cause 5 — Circular dependency graph exploding memory

Modules that import each other in a cycle can cause webpack/rollup to hold every intermediate parse tree in memory.

Diagnose:

npx madge --circular src/

If it finds 10+ cycles, that’s likely the memory cause.

Fix: refactor to break the cycles. Move shared types to a separate types.ts file that neither cycle-member imports.

Cause 6 — TypeScript compiling everything, including tests

If tsconfig.json doesn’t exclude node_modules, tests, or examples, tsc walks and holds them all in memory during type-check phase.

Fix:

{
  "exclude": [
    "node_modules",
    "**/*.test.ts",
    "**/*.spec.ts",
    "tests/**",
    "examples/**",
    ".next/**",
    "dist/**"
  ]
}

For big monorepos, use TypeScript project references — each project’s build is scoped.

Cause 7 — Old lockfile, dependencies bloated

Package versions drift over time. Transitive dependencies pile up, deduplication drifts, and node_modules bloats even when your direct dependencies didn’t change.

Fix — periodically clean-install:

rm -rf node_modules package-lock.json
npm install
npm audit fix

Commit the fresh lockfile. Vercel picks it up on next deploy.

When to just upgrade to Pro

If your build genuinely needs > 12 GB heap (large e-commerce catalog, 5000+ blog posts, heavy image processing), Pro plan removes the ceiling. The Hobby plan is designed for sites under a few hundred pages.

Signs it’s time:

  • OOM persists even at --max-old-space-size=7168
  • Build regularly takes over 30 minutes
  • You maintain 1000+ pages or MDX files
  • Team collaboration features become useful anyway

Pro is $20/month per member.

The recovery flow

Every Vercel OOM incident, in this order:

# 1. Read the error class: "Reached heap limit" vs "Killed"
# 2. Add NODE_OPTIONS=--max-old-space-size=7168, redeploy
# 3. If still fails: check page-generation count in the log
# 4. If image-heavy: pre-optimize before commit
# 5. If bundle-heavy: analyze and swap dependencies
# 6. If all above fail: upgrade plan or move to on-demand rendering

Step 2 alone fixes ~70% of cases.

Prevention checklist for new projects

Before the first deploy, set:

  1. NODE_OPTIONS=--max-old-space-size=7168 in Vercel env vars
  2. tsconfig.json exclude covers node_modules, tests, build artifacts
  3. Images pre-optimized before commit, WebP where possible
  4. Bundle analyzer runs at least once per major dependency change
  5. On-demand rendering enabled for archive/paginated content
  6. Lockfile refreshed every quarter

That baseline prevents almost every heap-out-of-memory build failure.

Bottom line

Vercel’s Reached heap limit error is Node.js hitting its self-imposed default heap ceiling (~2 GB), not Vercel’s 8 GB container limit. Set NODE_OPTIONS=--max-old-space-size=7168 and most builds recover instantly. If the OOM persists, the fix is upstream — too many static pages, unoptimized images, or bloated client bundles — and each has a specific remedy above.