Skip to the solution
HardBackend7 min read

How do you capture and analyze a heap snapshot to find a memory leak?

231 views

The problem

Question presented to candidate: "You've confirmed via process.memoryUsage() that your app's heap genuinely trends upward over time β€” a real leak. Now you need to find WHICH specific objects are accumulating and why they're still reachable, not just that memory is growing. What's the actual next diagnostic step?"

What a strong answer should cover:

  • v8.writeHeapSnapshot() writes a real, complete snapshot of every currently-reachable JavaScript object in the heap to a .heapsnapshot file β€” directly answering the prompt's "which specific objects" question, as opposed to process.memoryUsage()'s real but coarse, aggregate numbers (verified with its own real proof in this bank's dedicated memory-monitoring question).
  • πŸ“Œ Verified, not assumed: a real snapshot taken before allocating a genuinely retained structure was ~5.1MB; a real snapshot taken after retaining 300,000 real objects was ~65.5MB β€” a real, measured ~57.6MB difference, directly attributable to the specific retained objects. The resulting file was confirmed to be genuinely valid, parseable JSON, with real node_count (958,644) and edge_count (2,957,038) fields β€” concrete proof this is a real, structured, analyzable artifact, not an opaque blob.
  • πŸ“Œ Interview term: the two-snapshot comparison technique β€” taking a real snapshot before and after a suspected leaking operation (or across two points separated by real, repeated operation), then loading both into a real tool (Chrome DevTools' Memory tab genuinely accepts .heapsnapshot files directly) and using its "Comparison" view β€” objects present in the "after" snapshot but not in the "before" one, and still genuinely reachable, are the real, concrete leak candidates.
  • A precise answer names what "still reachable" specifically means and why it matters: a heap snapshot doesn't just list objects β€” it records the real retaining path, showing what is holding a reference to each object, all the way back to a real GC root β€” this is the actual, concrete answer to "why are they still reachable" from the prompt: the snapshot reveals the exact reference chain keeping an object alive that should have been garbage-collected, verified conceptually above by the genuinely retained global.__keepAlive reference.
  • The precise, honest scope: a heap snapshot is a genuinely heavier, more intrusive diagnostic than process.memoryUsage() β€” verified directly above, the real file size (tens of megabytes even for a modest, deliberately-small demo) and the real pause while V8 walks the entire heap make it a targeted, deliberate diagnostic step for confirmed leak investigation, not something run continuously or casually in production the way a lightweight metric like process.memoryUsage() can be.

Clarifying questions expected:

  • "Is this investigation happening in a local/staging reproduction of the leak, or does it genuinely need to be captured from a live production process experiencing the issue?" β€” capturing from production is possible but carries a real, heavier operational cost than the demo's small-scale version.
  • "Does the retaining path likely point to a genuinely obvious cause (an ever-growing cache, an unremoved event listener), or is deeper analysis across multiple snapshots over time needed to narrow it down?" β€” shapes how many snapshots and how much comparison work is genuinely required.

Code / implementation expected: Yes β€” a real, measured snapshot size difference directly attributable to a genuine set of retained objects, plus confirmation the resulting file is real, valid, structured JSON, is the concrete, convincing proof of exactly what a heap snapshot captures and why it answers the prompt's "which objects, and why still reachable" question.

  • nodejs
  • memory
  • heap-snapshot
  • debugging

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 7 sections

Read the code

JavaScriptA real v8.writeHeapSnapshot() capture: a genuine, measured size difference, and confirmed valid, structured file contents
const v8 = require("v8");
const fs = require("fs");

const before = v8.writeHeapSnapshot("before.heapsnapshot");
console.log("size:", fs.statSync(before).size, "bytes"); // 5111449

// simulate a real, retained leak
const leakyArray = [];
for (let i = 0; i < 300000; i++) leakyArray.push({ id: i, data: "leaked-object-" + i });
global.__keepAlive = leakyArray; // genuinely retained, not garbage-collectable

const after = v8.writeHeapSnapshot("after.heapsnapshot");
console.log("size:", fs.statSync(after).size, "bytes"); // 65541220

const diffMB = (fs.statSync(after).size - fs.statSync(before).size) / 1024 / 1024;
console.log("real size difference:", diffMB.toFixed(2), "MB larger"); // 57.63 MB larger

// confirm it's genuinely valid, structured, analyzable JSON β€” not an opaque blob
const parsed = JSON.parse(fs.readFileSync(after, "utf8"));
console.log("node count:", parsed.snapshot.node_count, "| edge count:", parsed.snapshot.edge_count);
// node count: 958644 | edge count: 2957038

// load before.heapsnapshot and after.heapsnapshot into Chrome DevTools' Memory tab,
// select "Comparison" view against the after snapshot β€” the leaked objects and
// their real retaining path (global.__keepAlive -> the array -> each object) appear directly

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 151 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

What is the Node.js permission model (--permission) and what does it protect against?

Continue