Note that while you don't have to explicitely deallocate memory in JS, memory leaks can still arise. At the same time, Node memory profiling utilities are (almost criminally) underdocumented. Let's find out how to use them.
TLDR: Skip ahead to the hands-on section with examples below, titled "Finding Memory Leaks (with Examples)".
Memory Leaks in JS
Since JS has a GC, memory leaks only have a few possibly causes:
You are hanging on to ("retaining") large objects, that are not used anymore, usually inside a variable in file or global scope. This is either accidental, or, part of a simplistic (indefinite) caching scheme:
let a;
function f() {
a = someLargeObject;
}
Sometimes objects are lingering in retained closures. E.g.:
let cb;
function f() {
const a = someLargeObject; // `a` is retained as long as `cb`
cb = function g() {
eval('console.log(a)');
};
}
You can easily fix such a memory leak by either never storing to, or by manually clearing those variables. However, when you are having a leak, the main difficulty is to find these lingering objects first.
Using Chrome Dev Tools to Profile Node Applications
Firstly, Node.js and Chrome both use the same JS engine: v8. Because of that, it was feasible for the Chrome Dev Tools team to add Node debugging and profiling support. While there are other tools available, Chrome Dev Tools (CDT) are probably more mature (and probably much better funded), which is why we will (for now) focus on how to use Chrome Dev Tools for Node memory profiling and debugging.
There are two main ways of profiling Node memory using CDT:
- Run your app with
--heap-prof
to generate a heap profile log file. Then load and analyze the log in CDT.
- (i) Run your app with
--inspect
/--inspect-brk
flag in order to debug your Node application in CDT. (ii) Connect the Chrome debugger to your node application via chrome://inspect/#devices. (iii) Use CDT's Memory
tab (documentation here) to your liking.
Method 1: heap-prof
Run your app with --heap-prof
to generate a heap profile log file. Then load and analyze the log in CDT.
Steps
- Run your application with
heap-prof
enabled. E.g.: node --heap-prof app.js
- Look into the working directory (usually the folder from where you are running the application). There is a new file which, by default, is named
Heap*.heapprofile
.
- Open a new tab in Chrome → open CDT → go to Memory tab
- At the bottom, press
Load
→ select Heap*.heapprofile
- Done. You can now see where memory, still alive at the end of the recording, was allocated.
Considerations for Method 1
This step allows you to, first of all, verify a memory leak, and find out what kind of allocations or objects might be causing it.
Let's look at CDT's memory profiling tool. It has three modes:

Sadly, the log recorded by --heap-prof
only contains data for mode 1. However, this mode is insufficient to answer the OP's third question: How can you find out why/where allocated objects are still lingering (that is: "retained" after not being used anymore)?
As explained in the tab: Answering that question requires the second mode.
I don't know if there is a hidden way to change the profile mode for Node, but I have not found it. I tried a few things, including adding from this list of undocumented Node.js CLI flags.
That is why @jmrk proposed method (2) in his answer:
Method 2: inspect
/inspect-brk
Run your app with --inspect
/--inspect-brk
flag in order to debug your Node application in CDT. Then just use CDT's Memory
tab (documentation here) to your liking.
Steps
- Run application in debug mode, and halt execution at the beginning:
node --inspect-brk app.js
- Open
chrome://inspect
in Chrome.
- After a few seconds, your application should show up in the list. Select it.
- CDT are launched and you see that execution is halted at the entry point of your application.
- Go to the Memory tab, select the 2nd mode and press the "Record" button
- Continue execution until the memory leak was recorded. For this, either put down a breakpoint somewhere, or, if the leak persists until the end, just let the app exit naturally.
- Go back to the Memory tab and press the "Record" button again to stop recording.
- You can now analyze the log (see below).
Considerations for Method 2
- Because you are now running your entire application in debug mode, everything is a lot slower.
- Heap Mode 2 generally requires a lot more memory. If memory exceeds your Node default memory limit (about 2gb), it will just crash. Monitor your memory usage, and possibly use something like
--max-old-space-size=4096
(or bigger numbers) to double the default. Or, even better, simplify your test case to use less memory and speed up profiling, if possible.
- The "Record Allocation Stacks" option shows you the call stack of when any object was allocated. That is similar to the functionality of Profile mode 1. It is not necessary for finding memory leaks. I have not needed it so far, but if you need to map the lingering objects to their allocations, this should help.
Finding Memory Leaks (with examples)
After following the steps of Method 2, you are now looking at all information you need to find your leak.
Let's look at some basic examples:
Example 1
Code
A simplistic memory leak is examplified in the code below: file-scoped a
stores data forever.
Complete Gist is here.
let a;
function test1() {
const b = [];
addPressure(N, b);
a = b;
gc(); // --expose-gc
}
test1();
debugger;
Notes:
- It is our goal to find "lingering" objects; which are "non-collectable" objects; objects that have been retained even though they are not used anymore. That is why I would usually call
gc
when profiling. This way we can make sure we get rid of all collectable references, and focus explicitly on the "lingering" objects.
- You need the
expose-gc
flag for the gc()
call; e.g.: node --inspect-brk --expose-gc app.js
Memory View
Once the breakpoint hits, I stop recording and I get this:

- The
Constructor
view lists all lingering objects, grouped by constructor/type.
- Make sure, you are sorting by
Shallow Size
or Retained Size
(both are explained here)
- We find that
string
is using up most memory. Let's open that up.
- Below every
Constructor
, you find a list of all it's individual objects. The first (biggest) object(s) is/are often times the culprit. Select the first.
- The
Retainers
view now shows you where this object is still being retained.
- Here you want to find the function that retained it for the long term (making it "linger").
Documentation on the Retainers
view is not quite complete. This is how I try to navigate it until it spits out the line of code that I'm looking for:
- Select an object.
- (Again, it's usually easiest to work through this list, sorted by size.)
- Inside the object's tree view entry: open up nested tree view entries.
- Look for anything refering to a line of code (displayed on the right-hand-side of the first column).
- Entries labeled with "context" might be more useful than others.
My findings are shown in this screenshot:

We see three functions playing a role in this object's lingering:
- The function that called
gc
- I'm not sure why this is. Probably related to GC internals. Might be because the gc
would cache references to some (if not all) lingering objects.
- The
addPressure
function allocated the object. This is also where the reference that retained it came from.
- The
test1
function is where we assigned the object to the file-scoped a
.
- This is the actual leak! We can fix it by either not assigning it to
a
, or make sure, we clear a
after it's not being used anymore.
Conclusion
I hope, this helps you get started on your exciting journey to finding and eradicating your memory leaks. Feel free to ask for more information below.