Avoiding Memory Leaks in Node.js: Why setInterval Can Be Dangerous and How to Fix It
Using setInterval for recurring calls in JavaScript might seem straightforward, but it comes with several pitfalls — especially in production systems or time-sensitive operations.
🚨 Memory Issues with setInterval
1. Closures Holding References
If your setInterval callback captures variables (closures), those references are retained indefinitely — preventing garbage collection.
function startInterval() {
const largeObject = new Array(1e6).fill('leak'); // ~50MB setInterval(() => { console.log(largeObject[0]); // Reference captured }, 1000); }
🔴 Problem: Even if largeObject is not used elsewhere, it stays in memory as long as the interval runs.
2. Uncleared Intervals
If you forget to clear unused intervals (e.g., on route change or component unmount), memory usage grows over time.
setInterval(() => {
// Memory usage grows here }, 1000); // But never: clearInterval(...)
✅ Always use clearInterval(id) when it's no longer needed.
3. Stacked or Overlapping Executions
If your interval callback runs longer than the interval itself, executions start stacking up — increasing memory and CPU usage.
setInterval(async () => {
await heavyOperation(); // takes 3s }, 1000); // new call every 1s → overlap!
🔁 Leads to:
- Too many concurrent function calls
- Event loop blocking
- Heap and private memory spikes
4. Node.js: Growing "Private Memory"
In Node.js, private memory increases if you create large objects or buffers inside setInterval:
setInterval(() => {
const buffer = Buffer.alloc(10 1024 1024); // 10MB every second }, 1000);
Even with global.gc() (if exposed), private memory held by closures or bindings may never be released unless you clear the interval or break the closure.
5. Missing Weak References
If you store objects inside the interval callback (or closures), they become strong references. Use WeakMap/WeakSet to avoid this.
🧠 Best Practices to Avoid Memory Leaks
The Safer Pattern: Recursive setTimeout
// Instead of setInterval:
async function runPeriodically() { try { await heavyOperation(); } finally { setTimeout(runPeriodically, 1000); // Only fires after task completes } } runPeriodically();
This guarantees the next execution only starts after the current one finishes — eliminating overlap entirely.
Originally published on LinkedIn
Backend slowing down as you grow?
Send me your stack and your traffic shape. I will tell you which of the four layers is actually your bottleneck — most teams optimise the wrong one and buy bigger servers instead.
Request a performance audit