Node.js "JavaScript heap out of memory": what it means and how to fix it
V8 ran out of old-space heap and aborted the process. Whether the fix is a flag or a leak hunt depends on what the memory curve looks like.
What this error actually means
"FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory" comes from V8, the JavaScript engine inside Node.js. V8 manages JavaScript objects in a garbage-collected heap whose largest region is the "old space". When an allocation fails and even a full mark-sweep collection can't free enough room within the configured limit, V8 aborts the entire process — there is no exception to catch. The limit depends on your Node version and available system memory, and can be raised explicitly with --max-old-space-size (in MB).
Note what the limit covers: only the V8 heap of JavaScript objects. Buffers, native addon allocations, and other off-heap memory live outside it — so a process can be killed by its container for total memory while the V8 heap looks fine, or crash on the V8 limit while the container still has gigabytes free. Diagnosing correctly means knowing which boundary you hit.
Common root causes
Leaks via closures, caches, and listeners
Closures capturing large objects that outlive the request, module-level maps and arrays used as unbounded caches, and event listeners added per request but never removed (the classic MaxListenersExceededWarning is an early hint). Each request leaves a little behind; hours later the old space is full.
Processing huge payloads in memory
Reading a multi-hundred-MB file with readFile, JSON.parse on a giant response, building a full result set in an array before responding — one big allocation spikes the heap past the limit. The fix is streaming: process data in chunks instead of materializing all of it.
Heap limit too low for a legitimate workload
Build tools, bundlers, and data-heavy services can genuinely need more old space than the default. If memory plateaus at a healthy level under normal load and only the heaviest legitimate operation crashes, the workload outgrew the default limit — raise it deliberately.
Buildup under load: promises and queues
When intake outpaces processing — unbounded concurrent promises, an in-memory queue without backpressure, responses accumulating faster than a slow downstream drains them — memory grows with traffic and collapses at peak. Not a classic leak: it drains when load stops, but crashes when it doesn't.
How to investigate and fix it
Establish the shape of the memory curve first — steady climb means leak, sharp spike means a big allocation — then use heap snapshots to put a name on what's retained.
- 1
Establish the memory growth pattern
Chart process.memoryUsage() (heapUsed vs rss) over time, or watch your monitoring. A floor that climbs steadily across hours is a leak; flat memory with a sudden spike at one operation is a single oversized allocation; growth that tracks traffic and drains afterward is missing backpressure.
- 2
Reproduce with the inspector attached
Run the service with node --inspect, open chrome://inspect, and drive realistic traffic at it. For production processes you can't restart, send SIGUSR1 to enable the inspector on a running process, or use a library to write heap snapshots to disk on demand.
- 3
Take heap snapshots over time and compare
Take a snapshot, apply load, take another, and use the comparison view in Chrome DevTools. Sort by retained size delta: the object types growing between snapshots — and the retainer chains showing exactly which variable holds them — are your leak, named and located.
- 4
Audit the usual suspects in code
Module-level collections that only grow, caches without max-size or TTL, listeners registered in request handlers, timers never cleared, and closures captured by long-lived objects (sockets, singletons). The retainer chain from the snapshot usually points directly at one of these.
- 5
Size the heap against the container
If the workload is legitimate, set --max-old-space-size to roughly 75–80% of the container's memory limit, leaving headroom for Buffers, native memory, and the stack. Setting it equal to the container limit invites the kernel OOM killer — a silent exit 137 instead of the V8 error.
- 6
Stream large data instead of buffering it
Replace readFile + parse with streams and incremental parsers, paginate database reads, and add backpressure (bounded queues, p-limit-style concurrency caps). After the fix, rerun the same workload and verify heapUsed stays bounded.
How to prevent heap out-of-memory crashes
- Monitor heapUsed and RSS per process and alert on a rising floor — leaks announce themselves long before the fatal error.
- Give every cache a max size and TTL (lru-cache or similar) — module-level Maps are where Node leaks go to hide.
- Stream files and large responses by default; treat any unbounded array of results as a bug waiting for production data.
- Set --max-old-space-size explicitly in containers, sized against the container limit with real headroom.
- Remove listeners and clear timers in cleanup paths, and treat MaxListenersExceededWarning as an error, not noise.
How AllStak helps with Node.js memory problems
AllStak's infrastructure monitoring charts memory per host and per container over time, which is exactly the view this error demands: a steadily climbing floor (leak), a one-operation spike (oversized allocation), or traffic-shaped growth (missing backpressure) each look different on the graph. Alerts on the trend give you days of warning instead of a fatal crash.
The Node.js SDK's error tracking captures the exceptions and restarts around the crash with release context, and centralized logs keep the fatal V8 message next to your process manager's restart lines. AllStak won't take heap snapshots for you — that's Chrome DevTools' job — but it tells you which process is growing, since when, and which deploy the growth started under.
Node.js heap out of memory — frequently asked questions
Should I just raise --max-old-space-size?
Only if the memory curve says undersizing: a stable floor with the crash occurring on a legitimately heavy operation. Against a leak, a bigger heap only postpones the crash and makes GC pauses longer. Check the trend first — raising the limit on a leaking service buys hours, not a fix.
Heap out of memory vs container OOMKilled — which do I have?
If you see the "FATAL ERROR ... JavaScript heap out of memory" message with a V8 stack, you hit the V8 limit. If the process vanished silently with exit code 137 and an oom-kill line in dmesg or kubectl describe, the kernel killed it for total memory — RSS including Buffers and native allocations, which --max-old-space-size doesn't govern.
Do Buffers count toward the V8 heap limit?
No. Buffer data lives in memory outside the V8 heap (reported as "external"/arrayBuffers in process.memoryUsage()), so a service drowning in Buffers can crash the container while heapUsed looks healthy. Watch RSS as well as heapUsed, and stream binary data instead of accumulating it.
How do I actually find the leak?
Heap snapshots, compared over time. Take one at baseline, apply load, take another, and sort the comparison by retained-size growth in Chrome DevTools. The growing object type plus its retainer chain — the path of references keeping it alive — names the exact variable and file. Two or three snapshot rounds usually settle it.
Explore more
By framework
Compare
Watch your Node processes before they crash
AllStak charts per-process and per-container memory, captures the errors around every restart, and keeps the logs that explain them — in one place.