Node.js heap out of memory, explained and fixed

Node stops a process that tries to grow the V8 heap past its limit and prints a fatal error ending in JavaScript heap out of memory. The limit exists so one process cannot consume the machine. Two very different problems produce this message: the process legitimately needs more memory than the default cap allows (large builds, big in-memory data), or it leaks, holding references so memory climbs until any cap would eventually fail. The fix depends entirely on which one it is.

Tell the two apart first

  1. Watch memory over time (a process monitor, or log process.memoryUsage().heapUsed periodically). A leak climbs steadily during steady work and never settles; a limit problem jumps when a big job arrives and is stable otherwise.
  2. Note the default cap: on 64-bit systems recent Node versions size the old-space limit relative to system memory (historically around 2 to 4 GB on large machines, much less on small containers). A process dying at a suspiciously round ceiling while the machine has free RAM is the limit, not the workload.
  3. Check what changed: a deploy that started dying after a data import, a dependency update, or moving to a smaller container points at workload or environment, a slow climb over days points at a leak.

The usual causes

  1. A genuine leak: a cache or map that only grows, event listeners added per request and never removed, closures retaining large objects, global arrays collecting results forever.
  2. Loading everything at once: reading a huge file into a string, fetching an entire table into memory, building a giant string by concatenation in a loop.
  3. Build tooling on large projects: bundlers, TypeScript, and test runners are famous for hitting the default cap on big codebases, the workload is legitimate and the default is simply small.
  4. A small container: the process's view of memory and the runtime's default heap do not account for the container limit the way the operator expects.

The fixes, matched to the cause

  1. For a legitimate workload, raise the cap deliberately: NODE_OPTIONS=--max-old-space-size=4096 (megabytes) in the service or build configuration, sized to what the machine or container actually has, and record why, an unexplained large cap hides the next leak.
  2. For a leak, find what retains the memory: capture two heap snapshots a few minutes apart under load (--inspect with Chrome DevTools, or v8.writeHeapSnapshot()) and compare, the growing constructor or collection names the culprit.
  3. Stream instead of loading: process files and query results in chunks with Node streams or cursors, so peak memory tracks the chunk size, not the dataset size.
  4. Bound every cache: give caches a maximum size or an eviction policy; an unbounded cache is a leak on a timer.
Raising --max-old-space-size on a leaking process only buys time, it converts a daily crash into a weekly one. If memory climbs monotonically during steady work, treat it as a leak until a snapshot comparison proves otherwise.

Common questions

What is the default heap limit in Node? It varies by Node version and platform; recent 64-bit builds scale it with total system memory, up to roughly 4 GB, while constrained environments get far less. Print it with node --v8-options or observe where the process dies.

Does this error occur in browsers too? Browsers manage per-tab memory differently and usually kill or reload the tab instead of printing this message. The wording is characteristic of Node and other V8 server runtimes.

Can garbage collection settings prevent it? Flags can tune collection timing, but they cannot create memory. If live data exceeds the cap, only a higher cap, less retained data, or streaming changes the outcome.

Related errors

Maximum call stack size exceeded · ERR_OSSL_EVP_UNSUPPORTED · Node: Cannot find module