← Debugging Guides

阅读中文版本

Node.js — JavaScript heap out of memory

Diagnose a Node.js heap exhaustion by capturing a heap snapshot, finding retained objects, and fixing the leak instead of only raising the heap limit.

advanceddebuggingnodejsmemory

Symptoms

  • FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
  • The process exits after memory climbs steadily for minutes or hours
  • The failure appears under load or after a large request, not at startup
  • Restarting buys time but the same pattern returns

Possible causes

  • A cache or map grows without eviction
  • Event listeners or timers are registered repeatedly and never removed
  • Large request or response bodies are buffered in memory instead of streamed
  • A closure retains objects that are no longer needed
  • The heap limit is genuinely too small for the workload

Diagnosis steps

Capture the heap limit and the process memory trend

node --max-old-space-size=4096 server.js

Interpretation

Raising the limit is a diagnostic step, not a fix. If memory still grows to the new ceiling, there is a leak or unbounded buffering.

Take a heap snapshot before and after the growth

node --heapsnapshot-signal=SIGUSR2 server.js

Interpretation

Send SIGUSR2 to write a snapshot, then compare retained sizes in Chrome DevTools Memory panel. The largest retained objects and their retainers identify the leak.

Reproduce with a focused load and inspect the dominant object types

node --inspect server.js

Interpretation

The inspector exposes live memory and allocation profiles. Look for arrays, buffers, or maps that grow with request count, not with warm-up.

Root cause

The V8 heap reached its limit because live objects exceeded the configured space. The cause is either an unbounded data structure or a workload that requires streaming rather than buffering.

Fix

Bound caches and maps, remove listeners and timers in cleanup, stream large bodies, release references after use, and paginate large queries. Raise the heap limit only when the live set is legitimately large and the host has memory headroom.

Risk note

Raising the limit without finding the leak delays the crash and may trigger the host OOM killer, which is harder to diagnose than a V8 heap error.

Verify the fix

Run the same load for longer than the original failure window and confirm memory reaches a plateau. Compare heap snapshots and verify the dominant retained objects stop growing. Add a memory alert before the limit is reached.

A heap error is a symptom

V8 reports that live objects exceeded the heap. It does not tell you which structure retained them. The first question is whether the live set is legitimately large—a batch job loading a huge file—or whether memory grows with every request. A steady climb that does not plateau under constant load points to a leak or unbounded buffering.

const cache = new Map(); // unbounded unless entries are evicted
app.get('/user/:id', async (req, res) => {
  const user = await loadUser(req.params.id);
  cache.set(req.params.id, user); // grows with every unique id
  res.json(user);
});

Find the retainer

Take a heap snapshot after warm-up and another after the growth period. In Chrome DevTools, compare the snapshots and sort by retained size. The retainer path shows why an object is still alive: a global map, an event emitter, a closure, or a timer. Fix the retainer, not the symptom. An LRU cache with a size limit, a listener removed on close, or a streamed response often removes the growth entirely.

const cache = new LRUCache({ max: 1000 });

Stream instead of buffer

Reading a large file or proxying a large response with await response.arrayBuffer() materializes the whole body. Use streams and backpressure. The same applies to query results: paginate or use a cursor rather than loading every row into an array.

Fix and verify

Bound every long-lived collection, clean up listeners and timers, and stream large payloads. Verify by running the original workload longer than the failure window and confirming memory plateaus. Compare snapshots to confirm the dominant retained type stops growing. Raise the heap limit only after the live set is understood and the host has headroom.