Node.js Memory Model — Stack, Heap & Garbage Collection Explained
When developers struggle with performance, async bugs, or memory leaks in Node.js, the root cause is usually one thing:
They don’t fully understand how Stack, Heap, Event Loop, and Garbage Collection work together.
This article builds a clean mental model — the kind that sticks.
1. The Call Stack — Where JavaScript Actually Executes
What It Is
The call stack is the memory area where JavaScript executes code.
It is managed by Google’s JavaScript engine V8, which powers Node.js.
It follows:
LIFO — Last In, First Out
What It Contains
- Function calls (stack frames)
- Local variables
- Function parameters
- Return addresses
Key Characteristics
- ⚡ Extremely fast
- 🔁 Automatically managed
- ⏳ Short-lived
- 📏 Limited in size
The Golden Rule
Only the call stack executes JavaScript code.
Not the event loop. Not the heap.
Only the stack.
Failure Mode: Stack Overflow
Deep or infinite recursion leads to:
RangeError: Maximum call stack size exceeded
Because the stack has limited space.
2. The Heap — Where Data Lives
If the stack is for execution, the heap is for storage.
What It Is
The heap is a large memory area for objects and long-lived data.
Also managed by V8, but through its Garbage Collector (GC).
What It Stores
- Objects
- Arrays
- Functions
- Closures
- Async callback references
Important Distinction
let a = 10; // value stored directly on stack
let obj = { x: 1 }; // reference on stack, object stored on heap
Characteristics
- 🐢 Slower than stack
- 📦 Dynamically allocated
- 🧹 Freed only by Garbage Collection
3. Stack vs Heap — Quick Comparison
| Aspect | Stack | Heap |
|---|---|---|
| Speed | Very fast | Slower |
| Size | Limited | Large |
| Lifetime | Until function returns | Until unreachable |
| Management | Automatic | Garbage collected |
| Stores | Execution | Data |
Simple memory mantra:
Stack executes. Heap stores.
4. The Event Loop — The Scheduler (Not Executor)
Many developers misunderstand the event loop.
What It Does
- It does NOT execute JavaScript
- It decides when callbacks are pushed to the stack
Who Executes JS?
✅ Call Stack (V8) ❌ Event Loop ❌ Heap
Mental Model
Async work happens elsewhere → Callbacks wait → Event loop pushes → Stack executes
In Node.js, async operations (timers, I/O) are handled by libuv, not by JavaScript itself.
5. What Actually Happens in Async Code
Consider:
setTimeout(() => console.log("timer"), 0);
Step-by-Step
- Stack sees
setTimeout - Timer is registered via libuv / OS
- Callback reference stored in heap
- Stack continues execution
- Event loop waits
- When eligible → callback pushed to stack
- Stack executes callback
Notice again:
The event loop never runs your code. It only schedules it.
6. Microtasks vs Macrotasks
Understanding this is critical for predicting execution order.
Macrotasks
setTimeoutsetInterval- I/O callbacks
setImmediate(Node.js)
Microtasks (Higher Priority)
Promise.thenqueueMicrotaskMutationObserver(browser)
Golden Rule
After the stack is empty, all microtasks run to completion before any macrotask runs.
Example
console.log("start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
Output
start
end
promise
timer
Why?
Because microtasks (Promises) run before macrotasks (timers).
7. Closures + Heap + Async — The Critical Concept
function greet() {
let name = "Abhi";
setTimeout(() => console.log(name), 1000);
}
greet();
Why Does name Survive?
- The stack frame disappears after
greet()finishes. - But the closure is stored in the heap.
- The closure still references
name. - Garbage Collector cannot free it.
📌 Stack dies. Heap survives.
This is powerful — and dangerous.
8. Garbage Collection in V8
Core Rule
Memory is freed only when it becomes unreachable from all GC roots.
What Are GC Roots?
- Global variables
- Active stack frames
- Timers & intervals
- Event listeners
- Pending async callbacks
- Unresolved promises
If something is reachable from a root, it cannot be collected.
Simplified GC Phases
- Mark → Traverse reachable objects
- Sweep → Free unmarked objects
9. Real Causes of Memory Leaks in Node.js
9.1 Closure Leaks
function start() {
let bigData = new Array(1_000_000).fill("*");
setInterval(() => {
console.log(bigData[0]);
}, 1000);
}
start();
Why it leaks:
- Interval is a root.
- Closure references
bigData. bigDatanever becomes unreachable.
9.2 Accidental Globals
cache = {}; // missing let/const
This attaches to global scope → becomes a GC root.
9.3 Event Listener Leaks
function setup() {
const bigBuffer = Buffer.alloc(50 * 1024 * 1024);
emitter.on("data", (msg) => {
process(bigBuffer, msg);
});
}
setup();
What You Expect
setup()finishes- Stack frame gone
bigBufferfreed
What Actually Happens
- Callback still exists
- Callback closes over
bigBuffer - Emitter holds reference to callback
bigBufferstays in heap forever
Correct Pattern
function handler(msg) {
process(msg);
}
emitter.on("data", handler);
// later
emitter.off("data", handler);
Always clean up listeners you no longer need.
9.4 Unbounded Caches
map.set(key, value); // no eviction policy
No limit = no release = memory growth.
10. Healthy GC vs Memory Leak
Healthy Behavior
- Heap grows
- GC runs
- Heap shrinks
Memory Leak
- Heap grows
- GC runs
- Heap never shrinks
That’s the diagnostic signal.
11. The One Question That Solves Most Leaks
Ask:
“What root is still holding a reference to this object?”
If you can draw this:
Root → closure → object
Then it is not collectible.
12. Final Mental Model (Memorize This)
Stack executes. Heap stores. Event loop schedules. GC frees unreachable memory. Leaks = unintended reachability.
13. Interview One-Liner (Perfect Answer)
If asked about closure memory leaks:
“A closure causes a memory leak when it remains reachable from a long-lived root, preventing the garbage collector from freeing the variables it closes over.”
Final Thoughts
Most Node.js performance problems are not about CPU.
They are about:
- Memory reachability
- Long-lived references
- Misunderstanding async scheduling
Once you deeply understand:
Stack → Heap → Event Loop → GC
You stop guessing.
You start reasoning.