V8’s heap isn’t just a memory dump; it’s a sophisticated, multi-generational garbage collector that actively tries to keep your Node.js app from consuming all your RAM.

Let’s watch V8 in action. Imagine we have a simple Express app that keeps a growing list of user IDs in memory:

const express = require('express');
const app = express();
const port = 3000;

const userIds = [];

app.get('/add-user', (req, res) => {
  const newId = Date.now();
  userIds.push(newId);
  console.log(`Added user: ${newId}. Total users: ${userIds.length}`);
  res.send(`User added. Total users: ${userIds.length}`);
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});

If we hit /add-user repeatedly, the userIds array grows. V8 will eventually kick in its garbage collector (GC) to reclaim memory from objects that are no longer reachable.

The core problem V8’s heap aims to solve is managing memory in long-running applications. Unlike short-lived scripts that just allocate and then exit, Node.js servers continuously allocate new objects. Without a garbage collector, this would quickly lead to out-of-memory errors. V8’s heap is designed to do this efficiently by dividing memory into generations.

When Node.js starts, it allocates a certain amount of memory for the heap. This heap is divided into two main spaces: the "new space" and the "old space." The new space is further divided into "from-space" and "to-space."

  1. Allocation: New objects are always allocated in the "to-space" of the new space. This is a very fast process, often just incrementing a pointer.
  2. Garbage Collection (Minor GC): When the "to-space" fills up, a minor garbage collection cycle occurs. V8 scans the "to-space" for objects that are still reachable (i.e., referenced by other objects). Reachable objects are then "copied" to the "from-space" of the new space. Unreachable objects are discarded, freeing up memory.
  3. Promotion: After a minor GC, the roles of "from-space" and "to-space" swap. Objects that survive a few minor GCs are considered "old" and are moved from the new space to the "old space."
  4. Garbage Collection (Major GC): The old space is collected much less frequently. When it needs to be collected, V8 performs a more thorough scan. This is more time-consuming but reclaims a larger amount of memory.

The beauty of this generational approach is that most objects in JavaScript are short-lived. They are allocated and become unreachable quickly, meaning they get garbage collected in the fast minor GCs in the new space. Only objects that live longer are promoted to the old space, which is scanned less often.

You can influence V8’s heap behavior using command-line flags. The most common ones relate to heap size.

  • --max-old-space-size=<size_in_mb>: This flag sets the maximum size of the old space. If your application consistently needs more memory than the default old space allows, you might see performance degradation as V8 struggles to free up memory or experiences excessive GC pauses. For example, to set the maximum old space to 4GB:

    node --max-old-space-size=4096 app.js
    

    This is useful when you have a predictable, large dataset that needs to reside in memory.

  • --max-new-space-size=<size_in_mb>: This flag controls the size of the new space. A larger new space can reduce the frequency of minor GCs, which might be beneficial if your application creates many short-lived objects. However, a larger new space also means minor GCs will take longer when they do occur. For example, to set the new space to 1GB:

    node --max-new-space-size=1024 app.js
    

    This is less commonly tuned than max-old-space-size.

To inspect heap usage, you can use the --trace-gc flag. This will print GC events to the console, showing you when GCs happen, how long they take, and how much memory is freed.

node --trace-gc app.js

Looking at the output, you’ll see lines like:

[1:46541:0x1234567890] Scavenge: 10.1 ms; [17200 / 20000 MB] [17200 / 20000 MB] [100 + 100]
[1:46541:0x1234567890] Mark-sweep: 25.5 ms; [18000 / 20000 MB] [18000 / 20000 MB] [100 + 100]

The first number is the GC type (Scavenge for minor, Mark-sweep for major). The millisecond value is the duration. The numbers in brackets show the heap size before and after GC, and the amount of memory freed.

The "new space" is actually comprised of two semi-spaces: "from-space" and "to-space." During a minor GC (Scavenge), objects are copied from the "to-space" to the "from-space." The crucial, often misunderstood, part is that V8 doesn’t just copy objects to any available space in the "from-space." It uses a technique called "evacuation" where it compacts the surviving objects. This means that even if an object has survived many minor GCs, it might still be moved to a different location within the "from-space" to reduce fragmentation. This compaction is what makes minor GCs so fast.

The next thing you’ll likely encounter is understanding how to profile memory leaks using the heap snapshot tool.

Want structured learning?

Take the full Nodejs course →