Node.js Memory Model — Stack, Heap & Garbage Collection Explained

By Abhijeet Verma · · Node.js Fundamentals

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

  1. Stack sees setTimeout
  2. Timer is registered via libuv / OS
  3. Callback reference stored in heap
  4. Stack continues execution
  5. Event loop waits
  6. When eligible → callback pushed to stack
  7. 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

  • setTimeout
  • setInterval
  • I/O callbacks
  • setImmediate (Node.js)

Microtasks (Higher Priority)

  • Promise.then
  • queueMicrotask
  • MutationObserver (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

  1. Mark → Traverse reachable objects
  2. 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.
  • bigData never 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
  • bigBuffer freed

What Actually Happens

  • Callback still exists
  • Callback closes over bigBuffer
  • Emitter holds reference to callback
  • bigBuffer stays 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.