Finding Memory Leaks in Node.js
A memory leak shows up as memory that keeps climbing until the process is killed or restarts. The fix starts with proving where the memory goes.
1. Confirm the leak
setInterval(() => {
const { heapUsed, rss } = process.memoryUsage();
console.log(`heap ${(heapUsed / 1e6).toFixed(0)} MB, rss ${(rss / 1e6).toFixed(0)} MB`);
}, 30_000).unref();
Memory that rises and falls is normal garbage collection. Memory whose low points keep rising under steady load is a leak.
2. Take heap snapshots
node --inspect src/server.js
# open chrome://inspect, click "inspect", Memory tab, "Take heap snapshot"
# or in production-like environments, write a snapshot on a signal:
node --heapsnapshot-signal=SIGUSR2 src/server.js
kill -USR2 <pid>
Take one snapshot, generate load, take another, then use the Comparison view to see which object types grew. Follow the Retainers panel to see what is holding them.
3. The usual suspects
Unbounded caches
const cache = new Map(); // grows forever
function getUser(id) {
if (!cache.has(id)) cache.set(id, loadUser(id));
return cache.get(id);
}
Use a size-limited LRU cache or a TTL.
Event listeners that are never removed
app.get('/stream', (req, res) => {
const onPrice = (p) => res.write(`data: ${p}\n\n`);
prices.on('update', onPrice);
req.on('close', () => prices.off('update', onPrice)); // without this, every client leaks
});
Other common causes
- Timers (
setInterval) that are never cleared. - Global arrays used as logs or queues.
- Closures holding large objects longer than needed.
- Promises that never settle, keeping their context alive.
4. Prevent regressions
Track heap usage in your monitoring with an alert on steady growth, and run a soak test (hours of steady traffic) before big releases.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.