Skip to content
Dev Tools Beginner Tutorial

Debug Node.js Memory Leaks with Chrome DevTools Heap Snapshots

Capture and diff heap snapshots to pinpoint exactly which objects are leaking in a running Node.js service.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Aug 10, 2026 · 4 min read
Debug Node.js Memory Leaks with Chrome DevTools Heap Snapshots

What you'll build / learn

You'll attach Chrome DevTools to a running Node.js service, capture two heap snapshots, and diff them to identify exactly which objects — and which line of code — are leaking memory.

Prerequisites

  • Node.js 18 or later — any current release works; this walkthrough was verified on Node 20.19 and applies unchanged to the 22 and 24 LTS lines.
  • Google Chrome (or any Chromium browser — use edge://inspect on Edge). Chrome DevTools ships with the browser; no install needed.
  • Any OS. One note for later: the kill -USR1 trick for attaching to an already-running process doesn't exist on Windows.

1. Create a leaky server

Save this as server.js. It simulates a classic real-world leak: a module-level "cache" that grows on every request and is never evicted.

const http = require('node:http');

const cache = [];

const server = http.createServer((req, res) => {
  cache.push({ url: req.url, body: Buffer.alloc(100 * 1024).toString('base64') });
  res.end(`handled ${cache.length} requests\n`);
});

server.listen(3000, () => console.log('listening on http://localhost:3000'));

2. Start it with the inspector enabled

node --inspect server.js

You'll see:

Debugger listening on ws://127.0.0.1:9229/c80a792d-5887-42bf-b084-414055b99f2f
For help, see: https://nodejs.org/en/docs/inspector
listening on http://localhost:3000

--inspect opens a debugging endpoint on 127.0.0.1:9229. For a service that's already running (and that you can't restart), send kill -USR1 <pid> instead — Node activates the same inspector on a live process.

3. Attach Chrome DevTools

  1. Open chrome://inspect in Chrome.
  2. Under Remote Target, find your server.js process and click inspect. (You can also click Open dedicated DevTools for Node — it auto-connects to localhost:9229.)
  3. In the DevTools window, open the Memory panel.

4. Take a baseline snapshot

In the Memory panel, select Heap snapshot and click Take snapshot. This is your "before" picture — the total size of all reachable objects appears under the snapshot in the left sidebar.

5. Generate load, then snapshot again

In a terminal, hit the server 500 times:

for i in {1..500}; do curl -s localhost:3000 > /dev/null; done

Back in DevTools, click the record icon (Take heap snapshot) again to capture Snapshot 2.

6. Diff the snapshots

  1. With Snapshot 2 selected, change the view dropdown at the top of the panel from Summary to Comparison, comparing against Snapshot 1.
  2. Sort by the Size Delta column, descending.
  3. The top offender is (string) — 500 new ~133 KB base64 strings that exist in Snapshot 2 but not Snapshot 1. The # New and # Delta columns both read ~500, matching your 500 requests. That correlation between "operations performed" and "objects added" is the leak signature.
  4. Expand (string), click one entry, and read the Retainers pane at the bottom. The chain reads roughly: body in Object → element in Arraycache in server.js. That's your culprit variable, named, with the file it lives in.

One metric worth knowing while you're here: Shallow Size is the memory an object holds itself; Retained Size is what would be freed if the object became unreachable. Sort by Retained Size when you want to find the object keeping everything else alive (here, the cache array).

Verify it works

You've found the leak when all three of these line up:

  • The Comparison view shows a constructor with # Delta ≈ your request count (here, ~500 (string) and ~500 Object entries).
  • Size Delta for that constructor accounts for most of the heap growth (~65 MB here — 500 × 133 KB).
  • The Retainers pane traces back to a variable you recognize — cache in server.js.

To confirm the fix: cap or evict the cache (e.g. if (cache.length > 100) cache.shift();), restart, rerun the 500-request loop, and re-diff. Size Delta for (string) should now hover near zero.

Troubleshooting

  • Starting inspector on 127.0.0.1:9229 failed: address already in use — another process already owns the inspector port. Kill it, or pick a different port with node --inspect=9230 server.js and add localhost:9230 under Configure… in chrome://inspect.
  • WebSockets request was expected — you opened http://localhost:9229 directly in a browser or with curl. That port speaks the DevTools protocol, not HTML; connect through chrome://inspect instead.
  • No Remote Target appears in chrome://inspect — check Discover network targets is enabled and click Configure… to confirm localhost:9229 is listed. If Node runs in Docker or on another machine, --inspect binds to 127.0.0.1 and is unreachable; use --inspect=0.0.0.0:9229 and publish the port — but only on trusted networks, since anyone who can reach the inspector can run arbitrary code in your process.
  • DevTools freezes or disconnects while snapshotting — snapshots serialize the whole heap, so multi-GB heaps take a while. Wait it out, or reproduce the leak with a smaller load so the heap stays snapshot-friendly.

Next steps

  • Use Allocation instrumentation on timeline (same Memory panel) to see allocations live as they happen, instead of diffing after the fact.
  • Snapshot production without DevTools attached: call v8.writeHeapSnapshot() or start with --heapsnapshot-signal=SIGUSR2, then load the .heapsnapshot file into the Memory panel offline.
  • Read the Chrome DevTools memory guide for the full tour of Containment and Statistics views.

Sources & further reading

  1. Debugging Node.js — nodejs.org
  2. Record heap snapshots — developer.chrome.com
  3. Fix memory problems — developer.chrome.com
  4. V8 - Node.js API documentation — nodejs.org
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading