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. CONFIDENCE : medium VERIFIED : 2026-08-08 (fresh, 0d old) URL : https://knowbase.sh/k/node-javascript-heap-out-of-memory ERROR ------------------------------------------------------------------------ FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory CODES: ERR_WORKER_OUT_OF_MEMORY 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 CAUSE ------------------------------------------------------------------------ 1. [primary] The workload legitimately needs more heap than V8 allows A 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 2. [primary] A genuine leak retains objects across requests A 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 3. [common] Data is loaded whole instead of streamed Reading 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 4. [common] The container limit is below the V8 heap setting Telling 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 5. [edge] Many small objects rather than a few large ones Millions 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 ------------------------------------------------------------------------ 1. Determine 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')" 2. If 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. # V8 heap ceiling in MiB NODE_OPTIONS=--max-old-space-size=3072 node app.js note: 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. 3. Make the two limits agree in the container definition, so neither surprises the other. resources: limits: memory: "4Gi" env: - name: NODE_OPTIONS value: "--max-old-space-size=3072" 4. If 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 note: Take one after warm-up and one an hour later, then load both into Chrome DevTools and compare by constructor. 5. Stream data instead of buffering it whole, so memory scales with concurrency rather than input size. // 🔴 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); 6. Bound every cache. An unbounded Map at module scope is a leak with extra steps. // 🔴 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. FALLBACK: 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 TO ------------------------------------------------------------------------ - 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 EVIDENCE ------------------------------------------------------------------------ 1. Node.js CLI — --max-old-space-size https://nodejs.org/api/cli.html OpenJS Foundation | official-docs | 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. 2. Resource Management for Pods and Containers https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ The Kubernetes Authors | official-docs | 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. CONFIDENCE ------------------------------------------------------------------------ medium — The 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. ------------------------------------------------------------------------ knowbase 0.1.0 — CC-BY-4.0