7 Common Garbage Collection Issues in Node.js

Garbage collection (GC) in Node.js is one of those things that developers often take for granted—until something goes wrong. Node.js relies on the V8 JavaScript engine, which includes an automatic garbage collector to manage memory. However, just because it’s automatic doesn’t mean it’s perfect.  Poorly managed garbage collection can lead to memory leaks, excessive CPU usage, and even application crashes.  1. Memory Leaks from Global Variables  The Problem  JavaScript allows developers to declare variables in the global scope, but in Node.js, this can cause memory leaks if variables are not properly released. Since global variables persist for the lifetime of the process, they won’t be garbage collected.  For example:   let cache = {}; // This stays in memory forever function storeData(key, value) {     cache[key] = value; } Here, every time storeData is called, memory usage increases because we keep adding values to cache, and it never gets freed.  The Solution  Avoid global variables whenever possible. Use let or const inside functions or objects to keep scope limited:   function createStore() {     let cache = {};      return {         set: (key, value) => cache[key] = value,         get: (key) => cache[key]     }; } const store = createStore(); This ensures that cache is only available within createStore(), allowing it to be garbage collected when no longer needed.  2. Detached DOM Elements in Server-Side Rendering  The Problem  When using server-side rendering (SSR) frameworks like Next.js, detached DOM elements can cause memory leaks. This happens when you remove elements from the DOM but keep references to them in JavaScript.  Example:   let removedElement = document.getElementById("someElement"); removedElement.remove();  Even though someElement was removed, it remains in memory because removedElement still holds a reference to it.  The Solution  Set references to null when elements are no longer needed:   let removedElement = document.getElementById("someElement"); removedElement.remove(); removedElement = null; // Allows GC to collect the memory If using React with SSR, ensure that you clean up event listeners when components unmount:   useEffect(() => {     const handler = () => console.log("Event triggered");     document.addEventListener("click", handler);         return () => document.removeEventListener("click", handler); }, []); 3. Memory Bloat from Large Objects and Buffers  The Problem  Handling large JSON objects or buffers can quickly overwhelm garbage collection in Node.js.  Example of a potential issue when processing large files:   const fs = require("fs");   fs.readFile("largeFile.txt", (err, data) => {     if (err) throw err;     console.log(data.toString()); }); Here, data holds the entire file in memory, which can be problematic for large files.  The Solution  Instead of reading the entire file into memory, use streams:   const fs = require("fs");   const stream = fs.createReadStream("largeFile.txt");   stream.on("data", chunk => {     console.log(chunk.toString()); }); Streams process data in chunks, reducing memory pressure and improving garbage collection efficiency.  4. Poor Handling of SetInterval and SetTimeout  The Problem  Timers (setInterval and setTimeout) create references in memory. If they are not properly cleared, they prevent garbage collection.  Example:   setInterval(() => {     console.log("Running every second"); }, 1000); This interval will run forever unless explicitly cleared.  The Solution  Always clear timers when they are no longer needed:   const interval = setInterval(() => {     console.log("Running..."); }, 1000);   setTimeout(() => {     clearInterval(interval); }, 5000); Here, clearInterval(interval) ensures that the function does not persist longer than necessary.  5. Circular References in Objects  The Problem  A circular reference occurs when two objects reference each other, preventing garbage collection.   let obj1 = {}; let obj2 = {};   obj1.ref = obj2; obj2.ref = obj1; Even if obj1 and obj2 are set to null, they remain in memory because they reference each other.  The Solution  Manually break circular references when objects are no longer needed:   obj1.ref = null; obj2.ref = null; Or use WeakMap, which allows objects to be garbage collected:   const weakMap = new WeakMap(); let obj = { data: "important" }; weakMap.set(obj, "some value");   // Once 'obj' is out of scope, it will be GC'd WeakMap does not prevent garbage collection when an object is no longer referenced elsewhere.  6. EventEmitter Memory Leaks  The Problem  Node.js’ EventEmitter allows multiple listeners. However, if you keep adding event list

Apr 6, 2025 - 05:16
 0
7 Common Garbage Collection Issues in Node.js

Garbage collection (GC) in Node.js is one of those things that developers often take for granted—until something goes wrong. Node.js relies on the V8 JavaScript engine, which includes an automatic garbage collector to manage memory. However, just because it’s automatic doesn’t mean it’s perfect. 

Poorly managed garbage collection can lead to memory leaks, excessive CPU usage, and even application crashes. 

1. Memory Leaks from Global Variables 

The Problem 

JavaScript allows developers to declare variables in the global scope, but in Node.js, this can cause memory leaks if variables are not properly released. Since global variables persist for the lifetime of the process, they won’t be garbage collected. 

For example:

 

let cache = {}; // This stays in memory forever
function storeData(key, value) {
    cache[key] = value; 
}

Here, every time storeData is called, memory usage increases because we keep adding values to cache, and it never gets freed. 

The Solution 

Avoid global variables whenever possible. Use let or const inside functions or objects to keep scope limited:

 

function createStore() {
    let cache = {};  
    return {
        set: (key, value) => cache[key] = value,
        get: (key) => cache[key]
    };
}
const store = createStore();

This ensures that cache is only available within createStore(), allowing it to be garbage collected when no longer needed. 

2. Detached DOM Elements in Server-Side Rendering 

The Problem 

When using server-side rendering (SSR) frameworks like Next.js, detached DOM elements can cause memory leaks. This happens when you remove elements from the DOM but keep references to them in JavaScript. 

Example:

 

let removedElement = document.getElementById("someElement");
removedElement.remove();  

Even though someElement was removed, it remains in memory because removedElement still holds a reference to it. 

The Solution 

Set references to null when elements are no longer needed:

 

let removedElement = document.getElementById("someElement");
removedElement.remove();
removedElement = null; // Allows GC to collect the memory

If using React with SSR, ensure that you clean up event listeners when components unmount:

 

useEffect(() => {
    const handler = () => console.log("Event triggered");
    document.addEventListener("click", handler);
    
    return () => document.removeEventListener("click", handler); 
}, []);

3. Memory Bloat from Large Objects and Buffers 

The Problem 

Handling large JSON objects or buffers can quickly overwhelm garbage collection in Node.js. 

Example of a potential issue when processing large files:

 

const fs = require("fs");
 
fs.readFile("largeFile.txt", (err, data) => {
    if (err) throw err;
    console.log(data.toString());
});

Here, data holds the entire file in memory, which can be problematic for large files. 

The Solution 

Instead of reading the entire file into memory, use streams:

 

const fs = require("fs");
 
const stream = fs.createReadStream("largeFile.txt");
 
stream.on("data", chunk => {
    console.log(chunk.toString());
});

Streams process data in chunks, reducing memory pressure and improving garbage collection efficiency. 

4. Poor Handling of SetInterval and SetTimeout 

The Problem 

Timers (setInterval and setTimeout) create references in memory. If they are not properly cleared, they prevent garbage collection. 

Example:

 

setInterval(() => {
    console.log("Running every second");
}, 1000);

This interval will run forever unless explicitly cleared. 

The Solution 

Always clear timers when they are no longer needed:

 

const interval = setInterval(() => {
    console.log("Running...");
}, 1000);
 
setTimeout(() => {
    clearInterval(interval);
}, 5000);

Here, clearInterval(interval) ensures that the function does not persist longer than necessary. 

5. Circular References in Objects 

The Problem 

A circular reference occurs when two objects reference each other, preventing garbage collection.

 

let obj1 = {};
let obj2 = {};
 
obj1.ref = obj2;
obj2.ref = obj1;

Even if obj1 and obj2 are set to null, they remain in memory because they reference each other. 

The Solution 

Manually break circular references when objects are no longer needed:

 

obj1.ref = null;
obj2.ref = null;

Or use WeakMap, which allows objects to be garbage collected:

 

const weakMap = new WeakMap();
let obj = { data: "important" };
weakMap.set(obj, "some value");
 
// Once 'obj' is out of scope, it will be GC'd

WeakMap does not prevent garbage collection when an object is no longer referenced elsewhere. 

6. EventEmitter Memory Leaks 

The Problem 

Node.js’ EventEmitter allows multiple listeners. However, if you keep adding event listeners without removing them, memory usage increases.

 

const EventEmitter = require("events");
const emitter = new EventEmitter();
 
setInterval(() => {
    emitter.on("event", () => console.log("Event fired"));
}, 1000);

Each interval adds a new listener, causing a memory leak. 

The Solution 

Remove listeners when they are no longer needed:

 

const listener = () => console.log("Event fired");
 
emitter.on("event", listener);
 
// Remove listener after use
emitter.removeListener("event", listener);

Or use .once() to automatically remove the listener after it fires:

 

emitter.once("event", () => console.log("Fires only once"));

7. Improper Use of Closures 

The Problem 

Closures allow functions to retain access to outer variables, but if not handled carefully, they can cause memory leaks. 

Example:

 

function createFunction() {
    let largeObject = new Array(1000000).fill("data"); 
    return () => console.log(largeObject.length);
}
 
const fn = createFunction(); 
// 'largeObject' remains in memory even though it's not used

Since fn keeps a reference to largeObject, it will never be garbage collected

The Solution 

Use closures carefully and avoid unnecessary references:

 

function createFunction() {
    let size = 1000000;  
    return () => console.log(size);
}
const fn = createFunction();

Now, only size is stored instead of a massive array. 

Key Takeaways: 

→ Avoid global variables that persist indefinitely. 
→ Use streams for large files instead of readFile()
→ Always clear unused setInterval and setTimeout timers. 
→ Break circular references manually or use WeakMap
→ Properly remove EventEmitter listeners. 
→ Be cautious with closures to avoid retaining unnecessary memory. 

You may also like:

  1. 10 Common Mistakes with Synchronous Code in Node.js

  2. Why 85% of Developers Use Express.js Wrongly

  3. Implementing Zero-Downtime Deployments in Node.js

  4. 10 Common Memory Management Mistakes in Node.js

  5. 5 Key Differences Between ^ and ~ in package.json

  6. Scaling Node.js for Robust Multi-Tenant Architectures

  7. 6 Common Mistakes in Domain-Driven Design (DDD) with Express.js

  8. 10 Performance Enhancements in Node.js Using V8

  9. Can Node.js Handle Millions of Users?

  10. Express.js Secrets That Senior Developers Don’t Share

Read more blogs from Here

Share your experiences in the comments, and let’s discuss how to tackle them!

Follow me on Linkedin