You know the moment. You reopen a long-running session in your AI coding CLI and instead of your conversation you get a wall of garbage-collector spam that ends like this:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
Aborted (core dumped)
Exit code 134, nothing partial to salvage, session unusable. The standard internet answer is "raise --max-old-space-size." I've watched that advice fail more than once, so here's what the message is really saying, and how to tell which of a few very different problems you're actually looking at.
Node aborts when V8's old generation can't grow any further. There are two versions of the message and they mean slightly different things:
Ineffective mark-compacts near heap limit means full mark-compacts are running and reclaiming almost nothing, so V8 stops trying.Reached heap limit means an allocation failed and the old generation cannot expand.
Both are terminal. Neither tells you why the heap filled up, which is the only part worth knowing.
If you've read that "Node caps the V8 heap at around 4 GB," that's only half true. V8 derives the default from your physical memory: roughly half of RAM, with a 2 GB floor value as the base default, and the cap only doubles to 4 GB on 64-bit builds with about 16 GB of RAM or more.
Print yours:
node -e "console.log(require('v8').getHeapStatistics().heap_size_limit/1024/1024 + ' MB')"
On the 13 GB Windows box I'm typing this on, that prints 2096 MB. So on a normal laptop the wall is 2 GB, not 4. (The number includes the young generation, so it comes out slightly above whatever --max-old-space-size value is in effect.)
When Node dies this way it prints the last few compactions first. Here's a real one from a Copilot CLI crash report:
Mark-Compact 4062.4 (4098.2) -> 4061.5 (4097.5) MB
Read the arrow. 4062 MB in, 4061 MB out. About a megabyte reclaimed out of four gigabytes. That is not a ceiling problem. Those objects are still referenced by the running process, and raising the ceiling just moves the wall a few gigabytes to the right. In that same thread someone tried NODE_OPTIONS=--max-old-space-size=8192 and got the other message instead: Reached heap limit. Same crash, later.
One more thing worth knowing: not every Node CLI honors that variable. Single-executable builds have been reported ignoring NODE_OPTIONS outright, so you can apply the popular fix and change nothing at all.
Retained objects. Reclaim per compact is near zero and heapUsed sits right at the limit. The tool is holding onto history, transcripts, or cached file contents.
A genuinely huge working set. The compacts do reclaim real memory and you still reach the ceiling. Here, raising the limit is the honest answer.
Something that isn't the JavaScript heap at all. rss climbs while heapUsed stays flat. That's native or FFI memory, and the heap flag is irrelevant to it. There's a Kilo CLI report with exactly this shape: RSS at 3.19 GB, peak around 4 GB, then a segfault inside an FFI trampoline after roughly 56 minutes. On Windows you might see the machine go down instead of the process: one Claude Code report has node.exe climbing to 45 GB and hard-locking the box, and raising the pagefile from 2 GB to 16 GB only stretched the crash from 7-13 minutes to about 3 hours. That's memory pressure, not a V8 cap.
Desktop apps add their own multiplier. Base64-inlining an attachment copies the file several times over. One report had a 238 MB PDF turn into about 317 MB of base64, then get copied again by the buffer and the JSON serialization, blowing the heap on an 8 GB machine.
heapUsed before changing any flag. Near-zero reclaim means stop resuming that session.NODE_OPTIONS=--max-old-space-size=6144. The host needs the RAM to back that up. rss is the number climbing, stop tuning V8 and go looking for a native leak or a Windows commit-charge problem.--heapsnapshot-near-heap-limit=1, open the snapshot in Chrome DevTools, and paste the exact FATAL line, the GC lines, the version, the OS, and whether the session was resumed. That's usually the difference between a report that gets fixed and one that gets closed.
One honest caveat about version bumps. In the Copilot CLI thread the maintainer pointed at a prerelease, a second user reproduced the crash on that prerelease anyway, and the changelog for a later build says long-running sessions "return freed memory to the system instead of holding gigabytes of it." So yes, upstream fixes for this class do exist and ship eventually. But the pointer alone wasn't enough, and there were about a dozen other issues in that repo carrying the same error string.
None of this needs a profiler to start. Read the arrow in the GC line first, because most of these crashes turn out not to be a cap problem at all.