This warning indicates that promises are being continuously created inside setInterval or setTimeout callbacks without proper cleanup, causing memory consumption to grow unbounded. The callback functions and their closures remain referenced, preventing garbage collection.
This warning appears when Node.js detects a pattern where promises are repeatedly created within timer callbacks (setInterval or setTimeout) without proper cleanup mechanisms. The issue stems from how Node.js's event loop and garbage collector interact with timer callbacks and promise chains. When you create promises inside a timer callback, each promise and its associated closure captures references to variables in its scope. If the timer continues running indefinitely, these references accumulate in memory. The garbage collector cannot reclaim this memory because the timer maintains active references to the callback function and everything it references. The problem is particularly severe with setInterval because it fires repeatedly, creating new promise chains on each iteration. Each promise chain holds references to callback functions, captured variables, and potentially large data structures. Over time, memory usage grows linearly with the number of timer executions, eventually exhausting available heap space and causing the application to crash or become unresponsive.
Use Node.js memory profiling tools to locate the source of the leak:
# Run your application with memory profiling
node --inspect server.js
# Or use heap snapshot
node --expose-gc --inspect server.jsOpen Chrome DevTools at chrome://inspect and take heap snapshots at different intervals. Look for objects that continuously increase in count, particularly:
- Closures created by timer callbacks
- Promise objects that accumulate over time
- Arrays or data structures captured in timer scopes
Example problematic pattern:
// BAD: Memory leak - promises accumulate
setInterval(() => {
const data = fetchData(); // Large data structure
Promise.resolve(data).then(result => {
processResult(result); // Closure captures 'data'
});
}, 1000); // Runs forever without cleanupCheck your code for setInterval or recursive setTimeout patterns that create promises continuously.
Always store the timer ID returned by setInterval/setTimeout and clear it when done:
// BEFORE: No cleanup
setInterval(() => {
fetchData().then(processResult);
}, 1000);
// AFTER: Proper cleanup
const intervalId = setInterval(() => {
fetchData().then(processResult);
}, 1000);
// Clear when done (on shutdown, component unmount, etc.)
function cleanup() {
clearInterval(intervalId);
}
// In Express.js shutdown
process.on('SIGTERM', () => {
cleanup();
server.close();
});
// In React component
useEffect(() => {
const intervalId = setInterval(() => {
fetchData().then(processResult);
}, 1000);
return () => clearInterval(intervalId); // Cleanup
}, []);Store all timer IDs in a collection if you have multiple timers:
const timers = new Set();
function createTimer() {
const id = setInterval(() => {
// Timer logic with promises
}, 1000);
timers.add(id);
return id;
}
function cleanupAllTimers() {
timers.forEach(id => clearInterval(id));
timers.clear();
}Instead of recursive promise chains with setTimeout, use async functions with controlled loops:
// BEFORE: Recursive promises - memory leak
function recursiveLoop() {
return Promise.resolve()
.then(() => doWork())
.then(() => new Promise(resolve => setTimeout(resolve, 1000)))
.then(recursiveLoop); // Infinite chain
}
// AFTER: Async function with controlled loop
async function asyncLoop() {
let running = true;
while (running) {
await doWork();
await new Promise(resolve => setTimeout(resolve, 1000));
// Add condition to exit loop
if (shouldStop()) {
running = false;
}
}
}
// Start with proper error handling
let loopController = asyncLoop();
// Stop the loop
function stopLoop() {
shouldStop = () => true;
}For more control, use AbortController pattern:
async function controlledLoop(signal) {
while (!signal.aborted) {
await doWork();
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
const controller = new AbortController();
controlledLoop(controller.signal);
// Stop when needed
controller.abort();Be mindful of what variables timer callbacks capture in their closure:
// BAD: Closure captures entire 'data' array
function startPolling() {
const data = []; // Grows indefinitely
setInterval(() => {
const newItem = fetchItem();
data.push(newItem); // Array keeps growing
Promise.resolve(data).then(processAll); // Captures entire array
}, 1000);
}
// GOOD: Don't capture growing data structures
function startPolling() {
const intervalId = setInterval(async () => {
const newItem = await fetchItem(); // Only current item
await processItem(newItem); // Process immediately
// No accumulation - newItem gets GC'd after processing
}, 1000);
return () => clearInterval(intervalId);
}
// BETTER: Pass only needed data, not entire context
function startPolling(config) {
const { url, interval } = config; // Extract only what's needed
const intervalId = setInterval(async () => {
const item = await fetch(url).then(r => r.json());
await processItem(item);
}, interval);
return () => clearInterval(intervalId);
}Store timer-associated data in WeakMap to allow garbage collection:
// BAD: Strong references prevent GC
const timerData = new Map();
function createTimer(resource) {
const id = setInterval(() => {
const data = timerData.get(id);
// Process data
}, 1000);
timerData.set(id, { resource, state: {} }); // Strong reference
}
// GOOD: WeakMap allows GC when timer is cleared
const timerData = new WeakMap();
function createTimer(resource) {
const timerObj = {}; // Use object as key
const id = setInterval(() => {
const data = timerData.get(timerObj);
if (data) {
// Process data
}
}, 1000);
timerData.set(timerObj, { resource, state: {} });
return () => {
clearInterval(id);
// timerObj can be GC'd after this function scope ends
};
}Implement memory monitoring to catch leaks early:
// Monitor heap usage
function logMemoryUsage() {
const used = process.memoryUsage();
console.log(`Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)}MB`);
console.log(`Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)}MB`);
}
// Log every 30 seconds
setInterval(logMemoryUsage, 30000);
// Advanced: Trigger alerts on high memory
function checkMemoryThreshold() {
const threshold = 500 * 1024 * 1024; // 500MB
const used = process.memoryUsage().heapUsed;
if (used > threshold) {
console.error(`Memory threshold exceeded: ${used / 1024 / 1024}MB`);
// Send alert, log to monitoring service
sendAlert('High memory usage detected');
}
}
setInterval(checkMemoryThreshold, 60000);Use production monitoring tools:
// With Prometheus client
const promClient = require('prom-client');
const memoryGauge = new promClient.Gauge({
name: 'nodejs_memory_heap_used_bytes',
help: 'Heap memory used in bytes'
});
setInterval(() => {
memoryGauge.set(process.memoryUsage().heapUsed);
}, 10000);Use Node.js built-in tools to verify the fix:
# Generate heap snapshots
node --inspect server.js
# Use clinic.js for detailed analysis
npm install -g clinic
clinic doctor -- node server.js
# Or use heapdump
npm install heapdumpIn your code:
// Take heap snapshot programmatically
const heapdump = require('heapdump');
// Before starting timers
heapdump.writeSnapshot('./before.heapsnapshot');
// After running for a while
setTimeout(() => {
heapdump.writeSnapshot('./after.heapsnapshot');
}, 60000);Compare snapshots in Chrome DevTools:
1. Open Chrome and go to chrome://inspect
2. Click "Open dedicated DevTools for Node"
3. Go to Memory tab → Load both snapshots
4. Compare to see what objects increased
5. Look for retained sizes of timer callbacks and promises
Run load tests to verify the fix:
# Monitor memory during load test
NODE_ENV=production node --max-old-space-size=512 server.js
# Memory should stabilize, not grow continuouslyAsyncLocalStorage and Hidden State: Node.js's AsyncLocalStorage API can exacerbate memory leaks by attaching hidden state to timeout and promise objects via internal symbols like Symbol(kResourceStore). These references persist until the timer executes, holding onto potentially large objects even after you think they should be garbage collected. If using AsyncLocalStorage with timers, be especially careful about object lifecycles.
The Timeout Object Primitive Bug: There's a known Node.js bug where converting timeout objects to primitives (using the unary + operator) causes unrecoverable memory leaks. Avoid patterns like +setTimeout(...) or +setInterval(...) when using AsyncLocalStorage.
Next.js Development Server Workaround: Next.js's development server works around these timer memory leak issues by periodically patching setTimeout and setInterval to forcefully clear intervals. This is why you might not see leaks in development but encounter them in production.
Promise Chain Best Practices: When using promises with timers, prefer Promise.all() over sequential promise chains if you have multiple operations. This allows garbage collection of completed promises rather than chaining them indefinitely:
// Better for GC
setInterval(async () => {
const results = await Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
]);
processResults(results);
}, 1000);Worker Threads for Long-Running Timers: For intensive background tasks with timers, consider moving them to worker threads. This isolates memory issues and allows the main thread to garbage collect independently:
const { Worker } = require('worker_threads');
const worker = new Worker('./timer-worker.js');
worker.on('message', handleResult);
// Terminate worker to cleanup
process.on('SIGTERM', () => {
worker.terminate();
});V8 Garbage Collection Tuning: For applications with heavy timer usage, you can tune V8's garbage collector:
# More aggressive GC
node --max-old-space-size=4096 --expose-gc server.js
# In code, manually trigger GC during low-load periods
if (global.gc) {
setInterval(() => {
if (isLowLoad()) {
global.gc();
}
}, 300000); // Every 5 minutes
}Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process