Skip to solution
hardBackend

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

231 views
01

Understand 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.

nodejsmemoryheap-snapshotdebugging
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js memory-diagnostics interviews — assumes familiarity with the memory-monitoring question's real process.memoryUsage() proof. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:</

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A 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
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 151 of 152 decoded in the Node.js track. One more won't hurt.

Back to track