Next.js Memory Leaks: Finding and Fixing Them
A practical guide to diagnosing memory leaks in Next.js applications — from App Router gotchas to third-party library traps, with Chrome DevTools workflows.
The Symptoms
Your Next.js app works fine on localhost. But in production, memory creeps up. After a few hours or days, the Node process exhausts its heap and crashes. Or worse: it doesn’t crash, it just gets progressively slower.
Common Causes in Next.js
1. Closures in Server Components
Server Components are rendered per-request. If you capture a large object in a closure that persists beyond the request, you’ve just leaked that data.
//Bad: cache object grows forever
const cache = new Map();
async function getData(id: string) {
if (cache.has(id)) return cache.get(id);
const data = await db.query(id);
cache.set(id, data); // never evicted
return data;
}
Fix: Use cache() from React or Next.js’s built-in unstable_cache with proper revalidation. Or implement eviction.
2. Event Listeners in useEffect
The classic React leak, but it still happens:
useEffect(() => {
window.addEventListener('resize', handler);
// Missing: return () => window.removeEventListener('resize', handler);
}, []);
Fix: Always return a cleanup function from useEffect if you subscribe to anything.
3. Third-Party Libraries That Hold References
Analytics SDKs, WebSocket clients, and charting libraries are frequent offenders. They register global listeners or maintain internal caches that grow without bounds.
Fix: Read the library’s docs for cleanup methods. If none exist, consider a wrapper that manages lifecycle.
4. Router Memory Leaks
The Next.js router can accumulate state if you’re not careful with useRouter() and dynamic routes. This is especially noticeable in apps with heavy client-side navigation.
Fix: Ensure you’re on the latest Next.js version — the team regularly patches memory leaks in the router.
5. Heap Snapshots That Never Clear
If you’re using generateStaticParams with large datasets and the data doesn’t get garbage collected between renders, your build process will consume more memory than necessary.
Fix: Process data in batches. Don’t hold all results in memory at once.
Diagnosing with Chrome DevTools
Step 1: Reproduce Consistently
Memory leaks that only appear in production are usually caused by server-side code. For client leaks:
- Open Chrome DevTools → Memory tab.
- Take a heap snapshot.
- Perform the action you suspect is leaking (navigate between pages, toggle a feature).
- Take another snapshot.
- Repeat 2-3 times.
- Select the last snapshot and change view to “Comparison.”
Step 2: Look for Retained Objects
Sort by “Retained Size.” Look for:
- Detached DOM nodes — elements removed from the document but still referenced by JS.
- Growing arrays or Maps — data structures that only grow, never shrink.
- Closure-scoped variables — objects kept alive by event handlers or callbacks.
Step 3: Trace the Retainers
Click an object in the comparison view and trace its retaining path. This tells you what is keeping it from garbage collection.
Server-Side Diagnosis
For Node.js memory leaks:
- Use
--heapsnapshot-signalflag (Node 18+):node --heapsnapshot-signal=SIGUSR2 server.js - Send
kill -USR2 <pid>to trigger a heap snapshot without restarting. - Load the
.heapsnapshotfile in Chrome DevTools.
Or use node-memwatch-next to track allocation trends over time.
Prevention Checklist
- ✅ Always clean up
useEffectsubscriptions. - ✅ Use React’s
cache()for server-side data deduplication. - ✅ Avoid global mutable state in server components.
- ✅ Audit third-party libraries for cleanup APIs.
- ✅ Set memory limits in production (
NODE_OPTIONS=--max-old-space-size=4096). - ✅ Monitor RSS and heap usage with APM tools.
- ✅ Load-test before deploying — tools like
artillerycan surface leaks under volume.
Memory leaks are silent. The first sign is often not an error but a slow, inevitable decline. Catch them early.