Quick answer
Node exhausted V8's heap. For a genuinely large job, raise the limit with --max-old-space-size=4096 (or NODE_OPTIONS for build tools). If memory steadily climbs in a long-running process, it is a leak — raising the limit only delays the crash.
The exact error string
<--- Last few GCs --->
[12345:0x...] 41523 ms: Mark-sweep 2047.8 (2068.1) -> 2047.0 (2068.6) MB
<--- JS stacktrace --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
1: 0x... node::Abort()
2: 0x... v8::internal::Heap::CollectGarbage(...)
...
The giveaway is the Last few GCs block: V8 ran garbage collection over and over, freed almost nothing (the numbers barely drop), and finally aborted.
Ineffective mark-compacts near heap limit
Just before the fatal error, V8 often prints Ineffective mark-compacts near heap limit Allocation failed. This is the warning sign, not a separate problem: a mark-compact is V8's most thorough garbage-collection pass, and “ineffective” means even that full sweep reclaimed almost no memory. V8 is spending more and more time in GC for less and less gain — the application is right up against the heap ceiling. If you catch this message in logs while the process is still alive, it is the moment to act before the crash: the fix is the same as for the fatal error below.
What the heap is
The heap is the region of memory where V8 stores your JavaScript objects, strings, arrays, and closures. V8 enforces a maximum heap size; when an allocation would exceed it and garbage collection cannot free enough room, V8 aborts the whole process rather than limp along in an unstable state. The crash is fatal and uncatchable — you cannot wrap it in a try/catch.
The default limit depends on your Node version and the machine's RAM. Older Node capped old-space around 2 GB on 64-bit systems; newer versions scale the default to physical memory — which is why a build can pass on a 16 GB laptop and fail in a 512 MB CI container.
Most common causes
The error shows up in two broad situations — heavy one-time work, and slow leaks. These are the cases developers hit most often:
- Webpack or Vite production builds on a large codebase
- Next.js static site generation (SSG) over many pages
- Loading a huge JSON file fully into memory with
JSON.parse - Reading an entire CSV into an array instead of streaming it
- Memory leaks in an in-process cache with no eviction
- Event listeners that are added but never removed
- Large database result sets loaded all at once instead of paginated
- Docker containers or CI runners with insufficient RAM
First, decide: large job or leak?
This single question determines the correct fix, and getting it wrong wastes hours:
- One-time spike (large job) — a big production build, bundling a large app, processing a huge JSON or CSV file. Memory rises once to a high peak. Fix: raise the heap limit.
- Steady climb (leak) — a long-running server whose memory grows over hours or days until it crashes, then crashes again after restart. Fix: find the leak. Raising the limit only postpones the crash.
Fix: raise the heap limit (for large jobs)
Give V8 a bigger old-space with --max-old-space-size, in megabytes. Run the script directly with the flag:
# 4 GB heap
node --max-old-space-size=4096 app.js
Most of the time the memory-hungry process is a build tool you do not invoke node for directly — webpack, tsc, Jest, Next.js, Vite. Pass the flag through the NODE_OPTIONS environment variable so every child Node process inherits it:
# macOS / Linux
NODE_OPTIONS=--max-old-space-size=4096 npm run build
# Windows (PowerShell)
$env:NODE_OPTIONS="--max-old-space-size=4096"; npm run build
# Make it durable in package.json:
{
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 webpack"
}
}
The cross-env in that package.json example sets the variable the same way on Windows, macOS, and Linux — install it first with npm install -D cross-env, or drop it if you only ever run the script on a single platform.
Only set the limit as high as the machine actually has RAM. Asking for 8192 on a 4 GB container will not help — the OS will kill the process (an OOM kill / exit code 137) before V8 reaches its limit.
One subtlety: --max-old-space-size caps only V8's old-space, not the entire process. Total resident memory (RSS) also includes the young generation, native buffers, and code, so a process you capped at 4096 can still show 5 GB+ in top or Task Manager. Leave headroom above the flag value when sizing a container.
JavaScript heap out of memory in Next.js, Vite, and Webpack
Build tools are the most common place this error appears, because bundling holds the whole module graph in memory. The flag goes in the same place for each — via NODE_OPTIONS on the build script — but the script names differ:
// package.json (cross-env installed: npm install -D cross-env)
{
"scripts": {
"build:next": "cross-env NODE_OPTIONS=--max-old-space-size=4096 next build",
"build:vite": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
"build:webpack": "cross-env NODE_OPTIONS=--max-old-space-size=4096 webpack",
"build:tsc": "cross-env NODE_OPTIONS=--max-old-space-size=4096 tsc -p ."
}
}
If the build still dies after raising the limit, the bundle itself is the problem, not the ceiling: look for huge dependencies pulled in whole, source maps on a giant codebase, or a Next.js SSG run generating thousands of pages at once. Splitting the build, disabling source maps in CI, or upgrading the bundler often does more than another gigabyte of heap.
Fix: it fails in Docker or CI
Containers and CI runners usually have far less memory than your laptop, and Node may size its default heap from the host instead of the container's cgroup limit. Two things to set:
- Give the container enough memory — raise the memory limit in your compose file, Kubernetes resources, or CI runner settings.
- Cap V8 to fit — set
NODE_OPTIONS=--max-old-space-sizeto a value comfortably below the container limit, leaving headroom for non-heap memory.
If you see exit code 137 rather than the V8 fatal error, the OOM killer terminated the process — the container itself is too small, regardless of the V8 flag.
Fix: find and stop the leak
If memory climbs steadily, capture a heap snapshot and compare snapshots over time to find what keeps growing. Node can dump one automatically just before the crash:
# Write a .heapsnapshot just before V8 hits the limit
node --heapsnapshot-near-heap-limit=1 app.js
# Or inspect a live process in Chrome DevTools
node --inspect app.js # then open chrome://inspect → Memory → take snapshots
Open the snapshot in Chrome DevTools → Memory and look for object types whose count or retained size grows between snapshots. The usual leak sources:
- Unbounded caches — a
Mapor object used as a cache with no eviction policy. - Leaked event listeners — listeners added on every request but never removed (watch for the
MaxListenersExceededWarning). - Closures holding large data — callbacks that capture big arrays or buffers and never release them.
- Accumulating arrays — pushing into a module-level array forever (logs, request history).
Reduce memory pressure
Whether it is a leak or a genuinely large workload, processing data more frugally often removes the need for a bigger heap entirely. Stream large files instead of reading them fully into memory; paginate database queries instead of loading every row; process records in batches; and release references when you are done so GC can reclaim them. A streaming JSON parser, for instance, keeps memory flat regardless of file size.
Debugging checklist
- ✓ Is this a one-time spike (build, big file) or a steady climb (long-running server)?
- ✓ Large job → raise
--max-old-space-size, orNODE_OPTIONSfor build tools - ✓ Set the limit below the machine's actual RAM — check for OOM exit code
137 - ✓ Fails in Docker/CI? Give the container more memory and cap V8 to fit
- ✓ Steady climb → capture a heap snapshot (
--heapsnapshot-near-heap-limit=1) and diff over time - ✓ Stream/paginate/batch large data instead of buffering it all in memory
Frequently Asked Questions
What does JavaScript heap out of memory mean?
It means the Node process tried to allocate more memory than V8's heap limit allows, so V8 aborted the process with a fatal error. The heap is where your JavaScript objects live. When garbage collection can no longer free enough space to satisfy an allocation, V8 gives up rather than continuing in an unstable state.
How do I increase the Node memory limit?
Raise V8's old-space limit with the --max-old-space-size flag, in megabytes. For example, node --max-old-space-size=4096 app.js gives a 4 GB heap. To apply it to a tool you do not call directly (webpack, tsc, jest), set it via the environment: NODE_OPTIONS=--max-old-space-size=4096. Only raise it as high as the machine's available RAM allows.
Should I just increase the memory limit or is it a leak?
If the job is legitimately large — a big production build, processing a huge file — raising --max-old-space-size is the right fix. If memory climbs steadily over time in a long-running server until it crashes, that is a memory leak and raising the limit only delays the crash. Watch the memory curve: a one-time spike means raise the limit; a steady climb means find the leak.
What is the default heap size in Node.js?
It depends on the Node version and available system memory. Older Node versions capped the old-space heap around 2 GB on 64-bit systems by default. Newer versions size the default heap based on the machine's physical RAM, so on a small container the default can be much lower than you expect — which is why builds that pass locally fail in CI or Docker.
Why does it happen in Docker or CI but not locally?
Containers and CI runners often have far less memory than your laptop, and Node may size its default heap from the host rather than the container's cgroup limit. The same build that has 16 GB locally might get 512 MB in CI. Set NODE_OPTIONS=--max-old-space-size to a value that fits the container, and make sure the container itself is given enough memory.
How do I find what is using the memory?
Take a heap snapshot. Run node with --heapsnapshot-near-heap-limit=1 to capture a snapshot just before the crash, or use the Chrome DevTools Memory tab against a --inspect session. Compare snapshots over time to find objects that keep growing — large arrays, caches without eviction, or event listeners that are never removed are the usual culprits.
Working with huge JSON files?
Validate and inspect JSON in your browser before loading it into Node.js — nothing is uploaded to a server.