DEV Community

Cover image for Tackling Memory Leaks in Node.js
Sergey Leschev
Sergey Leschev

Posted on β€’ Edited on

7 2 2 2 2

Tackling Memory Leaks in Node.js

Memory leaks in Node.js can be silent killers for your applications. They degrade performance, increase costs, and eventually lead to crashes. Let’s break down common causes and actionable strategies to prevent or fix them.

1️⃣ References: The Hidden Culprits

  • Global Variables: Accidentally assigning objects to global variables (e.g., global.data = ...) keeps them in memory forever.
// 🚨 Leak Example: Accidentally assigning to global scope  
function processUserData(user) {
  global.cachedUser = user; // Stored globally, never garbage-collected!
}
Enter fullscreen mode Exit fullscreen mode

Fix: Use modules or closures to encapsulate data:

// βœ… Safe approach: Module-scoped cache  
const userCache = new Map();  
function processUserData(user) {
  userCache.set(user.id, user);
}
Enter fullscreen mode Exit fullscreen mode
  • Multiple References: Unused objects retained by other references (e.g., caches, arrays).
// 🚨 Leak Example: Cached array with lingering references  
const cache = [];
function processData(data) {
  cache.push(data); // Data remains even if unused!
}
Enter fullscreen mode Exit fullscreen mode

Fix: Use WeakMap for ephemeral references:

// βœ… WeakMap allows garbage collection when keys are removed  
const weakCache = new WeakMap();  
function processData(obj) {
  weakCache.set(obj, someMetadata); // Auto-cleared if obj is deleted
}
Enter fullscreen mode Exit fullscreen mode
  • Singletons: Poorly managed singletons can accumulate stale data.

2️⃣ Closures & Scopes: The Memory Traps

  • Recursive Closures: Functions inside loops or recursive calls that capture outer scope variables.
// 🚨 Leak Example: Closure in a loop retains outer variables  
for (var i = 0; i < 10; i++) {
  setTimeout(() => console.log(i), 1000); // All logs print "10"!
}
Enter fullscreen mode Exit fullscreen mode

Fix: Use let or break the closure:

// βœ… let creates a block-scoped variable  
for (let i = 0; i < 10; i++) {
  setTimeout(() => console.log(i), 1000); // Logs 0-9
}
Enter fullscreen mode Exit fullscreen mode
  • require in the Middle of Code: Dynamically requiring modules inside functions can lead to repeated module loading.
// 🚨 Leak Example: Repeatedly loading a module  
function getConfig() {
  const config = require('./config.json'); // Re-loaded every call!
  return config;
}
Enter fullscreen mode Exit fullscreen mode

Fix: Load once at the top:

// βœ… Load once, reuse  
const config = require('./config.json');  
function getConfig() {
  return config;
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ OS & Language Objects: Resource Leaks

  • Open Descriptors: Unclosed files, sockets, or database connections.
// 🚨 Leak Example: Forgetting to close a file  
fs.open('largefile.txt', 'r', (err, fd) => {
  // Read file but never close fd!
});
Enter fullscreen mode Exit fullscreen mode

Fix: Always close resources:

// βœ… Cleanup with try-finally  
fs.open('largefile.txt', 'r', (err, fd) => {
  try {
    // Read file...
  } finally {
    fs.close(fd, () => {}); // Ensure cleanup
  }
});
Enter fullscreen mode Exit fullscreen mode
  • setTimeout/setInterval: Forgotten timers referencing objects.
// 🚨 Leak Example: Uncleared interval  
const interval = setInterval(() => {
  fetchData(); // Runs forever, even if unused!
}, 5000);
Enter fullscreen mode Exit fullscreen mode

Fix: Clear timers when done:

// βœ… Clear interval on cleanup  
function startInterval() {
  const interval = setInterval(fetchData, 5000);
  return () => clearInterval(interval); // Return cleanup function
}
const stopInterval = startInterval();
stopInterval(); // Call when done
Enter fullscreen mode Exit fullscreen mode

4️⃣ Events & Subscriptions: The Silent Accumulators

  • EventEmitter Listeners: Not removing listeners.
// 🚨 Leak Example: Adding listeners without removing  
const emitter = new EventEmitter();
emitter.on('data', (data) => process(data)); // Listener persists forever!
Enter fullscreen mode Exit fullscreen mode

Fix: Always remove listeners:

// βœ… Use named functions for removal  
function onData(data) { process(data); }
emitter.on('data', onData);
emitter.off('data', onData); // Explicit cleanup
Enter fullscreen mode Exit fullscreen mode
  • Stale Callbacks: Passing anonymous functions to event handlers (e.g., on('data', () => {...})).
// 🚨 Leak Example: Anonymous function in event listener  
const EventEmitter = require('events');
const myEmitter = new EventEmitter();

setInterval(() => {
  myEmitter.on('data', (message) => {
    console.log('Received:', message);
  });
}, 1000);

setInterval(() => {
  myEmitter.emit('data', 'Hello, world!');
}, 3000);
Enter fullscreen mode Exit fullscreen mode

Fix: Use once() for one-time events:

// βœ… Auto-remove after firing  
setInterval(() => {
  myEmitter.once('data', (message) => {
    console.log('Received:', message);
  });
}, 1000);
Enter fullscreen mode Exit fullscreen mode

5️⃣ Cache: A Double-Edged Sword

  • Unbounded Caches: Caches that grow indefinitely.
// 🚨 Leak Example: Cache with no limits  
const cache = new Map();
function getData(key) {
  if (!cache.has(key)) {
    cache.set(key, fetchData(key)); // Grows forever!
  }
  return cache.get(key);
}
Enter fullscreen mode Exit fullscreen mode

Fix: Use an LRU cache with TTL:

// βœ… npm install lru-cache
import LRUCache from 'lru-cache';
const cache = new LRUCache({ max: 100, ttl: 60 * 1000 }); // Limit to 100 items, 1min TTL
Enter fullscreen mode Exit fullscreen mode
  • Rarely Used Values: Cache entries that are never accessed.

6️⃣ Mixins: The Risky Extensions

  • Messing with Built-ins: Adding methods to Object.prototype or native classes.
// 🚨 Leak Example: Adding to Object.prototype  
Object.prototype.log = function() { console.log(this); };  
// All objects now have `log`, causing confusion and leaks!
Enter fullscreen mode Exit fullscreen mode

Fix: Use utility functions instead:

// βœ… Safe utility module  
const logger = {
  log: (obj) => console.log(obj)
};
logger.log(user); // No prototype pollution
Enter fullscreen mode Exit fullscreen mode
  • Process-Level Mixins: Attaching data to process or global contexts.

7️⃣ Concurrency: Worker & Process Management

  • Orphaned Workers/Threads: Forgetting to terminate child processes or Worker threads.
// 🚨 Leak Example: Forgetting to terminate a worker  
const { Worker } = require('worker_threads');
const worker = new Worker('./task.js');  
// Worker runs indefinitely!
Enter fullscreen mode Exit fullscreen mode

Fix: Track and terminate workers:

// βœ… Cleanup with a pool  
const workers = new Set();
function createWorker() {
  const worker = new Worker('./task.js');
  workers.add(worker);
  worker.on('exit', () => workers.delete(worker));
}
// Terminate all on shutdown  
process.on('exit', () => workers.forEach(w => w.terminate()));
Enter fullscreen mode Exit fullscreen mode
  • Shared State in Clusters: Memory duplication in multi-process setups.

πŸ”§ Pro Tips for Prevention

  • Heap Snapshots: Use node --inspect + Chrome DevTools to compare heap snapshots.
  • Monitor Event Listeners: Tools like emitter.getMaxListeners() or EventEmitter.listenerCount() to find leaks.
  • Automate Cleanup: Use destructors, finally blocks, or libraries like async-exit-hook for resource cleanup.

Memory leaks are inevitable in complex systems, but with vigilance and the right practices, you can keep them in check. πŸ’‘

Top comments (0)

Visualizing Promises and Async/Await 🀯

async await

Learn the ins and outs of Promises and Async/Await!