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!
}
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);
}
- 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!
}
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
}
- 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"!
}
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
}
-
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;
}
Fix: Load once at the top:
// β
Load once, reuse
const config = require('./config.json');
function getConfig() {
return config;
}
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!
});
Fix: Always close resources:
// β
Cleanup with try-finally
fs.open('largefile.txt', 'r', (err, fd) => {
try {
// Read file...
} finally {
fs.close(fd, () => {}); // Ensure cleanup
}
});
- setTimeout/setInterval: Forgotten timers referencing objects.
// π¨ Leak Example: Uncleared interval
const interval = setInterval(() => {
fetchData(); // Runs forever, even if unused!
}, 5000);
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
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!
Fix: Always remove listeners:
// β
Use named functions for removal
function onData(data) { process(data); }
emitter.on('data', onData);
emitter.off('data', onData); // Explicit cleanup
- 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);
Fix: Use once() for one-time events:
// β
Auto-remove after firing
setInterval(() => {
myEmitter.once('data', (message) => {
console.log('Received:', message);
});
}, 1000);
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);
}
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
- 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!
Fix: Use utility functions instead:
// β
Safe utility module
const logger = {
log: (obj) => console.log(obj)
};
logger.log(user); // No prototype pollution
- 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!
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()));
- 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)