# Node.js FATAL ERROR: JavaScript heap out of memory
V8 hit its heap ceiling and gave up. Two different ceilings can cause it — V8's own --max-old-space-size and the container's memory limit — and they fail differently. Raising the wrong one either does nothing or gets the process killed instead.
| Error | FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory |
|---|---|
| Applies to | Node.js 12 and later · V8 all versions shipped with Node.js |
| Primary cause | The workload legitimately needs more heap than V8 allows |
| First check | node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024, 'MiB heap limit')" |
| Confidence | medium2 sources, 2 primary |
| Verified | 2026-08-08fresh0d old · recheck by 2027-08-08 |
| Domain | languagenodejs v8 memory heap containers performance |
## Error
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryCodes: ERR_WORKER_OUT_OF_MEMORY
Also seen as: Reached heap limit Allocation failed · JavaScript heap out of memory · Last few GCs · ineffective mark-compacts near heap limit
## Problem
A process that works on a developer machine dies in CI or production, or a long-running service crashes after hours of normal operation. The stack trace is V8's rather than the application's, so it names no useful line. Raising the memory limit sometimes helps and sometimes changes nothing, which makes the boundary look arbitrary.
## Root Cause5 known causes, ranked
- 01
The workload legitimately needs more heap than V8 allows
primaryA large build, a bulk import, or a sizeable JSON parse. V8's old-space limit is a deliberate ceiling rather than a reflection of available RAM, and it is often smaller than the machine's memory.
→ how to tell: The failure is reproducible at a specific input size, and raising --max-old-space-size moves the boundary rather than merely delaying it
- 02
A genuine leak retains objects across requests
primaryA module-level cache without eviction, listeners added per request and never removed, or a closure holding a large buffer. Heap usage grows with uptime rather than with the current request, so any ceiling is reached eventually.
→ how to tell: Heap after a forced GC grows monotonically over hours, and two heap snapshots taken an hour apart show the same constructor growing
- 03
Data is loaded whole instead of streamed
commonReading a file, an HTTP response, or a query result into memory at once. Memory then scales with the input rather than with concurrency, which works fine until the input grows.
→ how to tell: Peak memory tracks input size almost exactly, and the code uses readFile, res.json or an unpaginated query rather than a stream or cursor
- 04
The container limit is below the V8 heap setting
commonTelling V8 it may use 4 GB inside a 1 GB container means the kernel kills the process before V8 ever reaches its own limit. The symptom then changes from a heap error to exit code 137, which sends you looking in the wrong place.
→ how to tell: The container reports OOMKilled with exit code 137 rather than printing the V8 fatal error, and --max-old-space-size exceeds the memory limit
- 05
Many small objects rather than a few large ones
edgeMillions of small objects, strings or closures can exhaust the heap while no single allocation looks large, and they make garbage collection progressively more expensive before the failure.
→ how to tell: The log shows repeated ineffective mark-compacts and rising GC pause times before the fatal error
## Solution
- 01Determine which ceiling you hit. A printed V8 fatal error means V8's limit; a silent death with exit code 137 means the kernel or container limit.
$ node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024, 'MiB heap limit')" - 02If the workload genuinely needs more heap, raise V8's limit — but keep it below the container's memory limit, with room for everything outside the heap.bash
# V8 heap ceiling in MiB NODE_OPTIONS=--max-old-space-size=3072 node app.jsnote: On a 4 GiB container, 3072 leaves headroom for buffers, native memory and the runtime itself. Setting it at or above the container limit converts a recoverable heap error into a SIGKILL.
- 03Make the two limits agree in the container definition, so neither surprises the other.yaml
resources: limits: memory: "4Gi" env: - name: NODE_OPTIONS value: "--max-old-space-size=3072" - 04If memory grows with uptime, take heap snapshots and compare rather than guessing. The retained-size delta names the leak.
$ node --heapsnapshot-signal=SIGUSR2 app.js # then: kill -USR2 <pid>note: Take one after warm-up and one an hour later, then load both into Chrome DevTools and compare by constructor.
- 05Stream data instead of buffering it whole, so memory scales with concurrency rather than input size.javascript
// 🔴 whole file in memory const data = await fs.readFile("huge.ndjson", "utf8"); for (const line of data.split("\n")) process(line); // ✅ constant memory regardless of size const rl = readline.createInterface({ input: createReadStream("huge.ndjson") }); for await (const line of rl) process(line); - 06Bound every cache. An unbounded Map at module scope is a leak with extra steps.javascript
// 🔴 grows forever const cache = new Map(); // ✅ bounded, with eviction import { LRUCache } from "lru-cache"; const cache = new LRUCache({ max: 5000, ttl: 60_000 });
verify · The process completes the workload that previously failed, and over a sustained run heap_used_size after garbage collection returns to a stable baseline rather than climbing.
if that fails · For a genuinely large one-off job that cannot be streamed, move it out of the request path into a worker or a separate process with its own generous limit, so an out-of-memory failure there cannot take the service down with it.
## Applies To
- Node.js
- 12 and later— --max-old-space-size sets V8's old-space ceiling in MiB; it is independent of, and can exceed, the memory actually available.
- V8
- all versions shipped with Node.js
- Runtimes
- Node.js, containers
## Not Applicable Tonear misses this page does not answer
- ✗ Exit code 137 with no V8 error printed, which is the kernel OOM killer rather than V8's own limit
- ✗ Native or off-heap memory growth such as Buffers, which is not governed by the heap limit
- ✗ Browser tab crashes, where the limit is set by the browser rather than by a flag
- ✗ Slow performance from garbage-collection pressure without an actual failure
## Evidence2 sources
https://nodejs.org/api/cli.html
OpenJS Foundation · read 2026-08-08
supports: That the flag sets V8's old-space ceiling, that approaching it makes V8 spend increasing time in garbage collection before failing, and that the value should be set below available memory to leave room for other uses — which is exactly why it must stay under a container limit.
“Sets the max memory size of V8's old memory section. As memory consumption approaches the limit, V8 will spend more time on garbage collection in an effort to free unused memory.”
https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
The Kubernetes Authors · read 2026-08-08
supports: That a container exceeding its memory limit is killed by the kernel OOM killer — the reason a V8 heap ceiling set above the container limit produces a SIGKILL instead of a catchable heap error.
“memory limits are enforced by the kernel with out of memory (OOM) kills”
## Confidence
mediumThe flag's meaning, V8's escalating garbage collection near the limit, and the guidance to stay below available memory are quoted from Node's own CLI reference; the container interaction is quoted from Kubernetes. Confidence is medium rather than high because it rests on two sources: the snapshot-comparison workflow and the streaming and cache remedies are established practice rather than statements quoted here.