Real-world examples of common causes of memory leaks in JavaScript
Examples of common causes of memory leaks in JavaScript you actually see in production
Before talking theory, let’s start with situations you’ve probably hit in real projects. These examples of common causes of memory leaks in JavaScript show up again and again in code reviews and performance incidents:
- A React single-page app that gets slower every time you navigate between routes.
- A Node.js API that needs to be restarted every few days because memory usage keeps climbing.
- A dashboard that’s fine for the first 10 minutes, then starts stuttering as charts update.
In almost every case, the pattern is the same: something that should be garbage-collected is still referenced from somewhere. Your job is to find that “somewhere.”
Let’s walk through specific, real examples and how to fix them.
Example of event listener leaks in single-page applications
One of the best examples of a memory leak in modern JavaScript is the event listener that never gets removed.
Imagine a single-page app where you attach listeners manually:
function attachSearchHandlers() {
const input = document.getElementById('search');
const button = document.getElementById('search-btn');
const handler = () => performSearch(input.value);
button.addEventListener('click', handler);
}
``
If this runs every time a route is mounted, but you never call `button.removeEventListener('click', handler)`, then every visit to that route adds another listener. Even if your routing library swaps out DOM nodes, you may still be holding references through closures or global arrays.
**Why this leaks:**
- The `button` element is referenced by the event system.
- The `handler` closure references `input` and whatever else is in scope.
- As long as the listener is registered, those objects cannot be garbage-collected.
In React, Vue, or Angular, this often happens when developers mix direct DOM access (`document.getElementById`, `addEventListener`) with the framework’s lifecycle and forget cleanup.
**Fix:**
- Use the framework’s lifecycle hooks to clean up listeners (`useEffect` cleanup in React, `onUnmounted` in Vue, etc.).
- Or centralize event handling so you don’t attach new listeners on every render.
This is one of the clearest **examples of common causes of memory leaks in JavaScript** in SPAs built between 2020–2025 as apps moved to long-lived, never-reloading shells.
---
## Closure traps: examples include timers and intervals that never die
Closures are powerful, but they’re also a classic **example of** how you can accidentally pin a lot of memory.
Consider a polling function:
```js
function startPolling(apiClient) {
const cache = new Map();
setInterval(async () => {
const data = await apiClient.fetchData();
cache.set(Date.now(), data);
}, 5000);
}
This looks harmless, but there are two problems:
setIntervalis never cleared.- The closure captures
cacheandapiClient, so they stay alive forever.
If startPolling is called multiple times (for example, every time a component mounts), you now have multiple intervals and multiple cache maps, none of which are freed. Over a few hours, the process’s memory usage climbs steadily.
Fix:
- Store the interval ID and call
clearIntervalwhen you no longer need it. - Avoid unbounded caches; limit size or use WeakMaps where appropriate.
This pattern is frequently visible in Chrome DevTools’ Memory panel as a steady increase in detached closures and retained objects. It’s one of the best examples of common causes of memory leaks in JavaScript backends as well, especially in long-running Node.js workers.
Detached DOM nodes: a textbook example of leaking the UI
Detached DOM nodes are a classic example of common causes of memory leaks in JavaScript on the front end.
Imagine a table that refreshes every 5 seconds:
let rows = [];
function renderTable(data) {
const table = document.getElementById('users');
table.innerHTML = '';
data.forEach(user => {
const tr = document.createElement('tr');
tr.textContent = user.name;
table.appendChild(tr);
rows.push(tr); // debugging leftover
});
}
That rows array started as a debugging aid: “Let’s keep references so we can inspect them.” Then it stayed. Every time new data comes in, you create new <tr> elements and push them into rows. Even though you clear the table’s inner HTML, those old nodes are still strongly referenced in your rows array.
Result:
- The DOM is detached, but the nodes are not garbage-collected.
- Heap snapshots show lots of
HTMLTableRowElementinstances retained by an array.
Fix:
- Don’t store DOM references longer than necessary.
- If you must, clear the collection (
rows.length = 0) when you re-render.
In 2024–2025, this is still one of the most common findings when engineers run a basic memory profiling session on an older dashboard or admin panel.
Global variables and long-lived singletons: subtle but persistent leaks
Global state and singletons are convenient, but they’re also easy examples of common causes of memory leaks in JavaScript when they become accidental “object graveyards.”
Picture a global registry:
const sessions = {};
export function trackSession(id, data) {
sessions[id] = data;
}
If you never delete old entries, sessions grows forever. Maybe each data object includes user preferences, cached API responses, and DOM references. Now every user who has ever touched the app is still in memory.
In Node.js, the same pattern appears in:
- Caches attached to
globalor to imported singletons. - Module-level arrays that keep app-level state.
Fix:
- Implement explicit eviction: LRU caches, time-based expiration, or maximum size.
- Use
WeakMapfor keys that should not prevent garbage collection.
The key point: any long-lived object (global, singleton, or module-level) is a candidate for leaks if it stores references to short-lived data and never drops them.
EventEmitter and Pub/Sub leaks in Node.js services
On the server side, one of the best examples of memory leaks is misused EventEmitter or pub/sub systems.
Example in Node.js:
import EventEmitter from 'events';
const bus = new EventEmitter();
export function registerUserListener(userId) {
const handler = (event) => {
if (event.userId === userId) {
// handle event
}
};
bus.on('user-event', handler);
}
If listeners are never removed when a user disconnects, you end up with one listener per user per connection. Over time, the process accumulates thousands of listeners, each capturing some state.
Node will even warn you: MaxListenersExceededWarning. Many teams ignore this until memory usage spikes.
Fix:
- Use
bus.offorbus.removeListenerwhen a connection closes. - Consider using
oncefor one-time listeners. - Track listener counts and set reasonable limits.
This is a very real example of common causes of memory leaks in JavaScript in microservice architectures, especially with WebSocket servers or long-lived streaming connections.
Leaky caches and in-memory storage: real examples from 2024–2025
As more teams move to microservices and serverless, there’s a strong temptation to speed things up with in-memory caches. That’s fine, but unbounded caches are textbook examples of common causes of memory leaks in JavaScript.
A simplified Node.js cache:
const cache = new Map();
export async function getUser(id) {
if (cache.has(id)) return cache.get(id);
const user = await db.fetchUser(id);
cache.set(id, user);
return user;
}
Under load, id might be almost unique per request (think UUIDs, session IDs). Without eviction, cache grows until the process hits its memory limit.
In 2024–2025, this pattern shows up in:
- Edge functions trying to “warm” data across invocations.
- Long-lived Node.js workers that process queues.
- Analytics pipelines that cache large JSON blobs.
Fix:
- Use a real cache with eviction (Redis, Memcached) or an LRU cache library.
- Cap the number of entries and drop the oldest.
- Avoid caching huge objects; cache IDs or small summaries instead.
The leak here is not a bug in the garbage collector; it’s your logic never letting go of references.
Framework-specific examples of common causes of memory leaks in JavaScript
Modern frameworks help, but they don’t magically prevent leaks. A few real examples from React, Vue, and Angular:
React: effects that never clean up
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
}, []); // missing cleanup
If the component unmounts and remounts in certain navigation flows, you can end up with multiple active intervals. The fix is to return a cleanup function:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
Vue: watchers that outlive their components
In Vue 2, manually created watchers or event buses can outlive components if you don’t tear them down in beforeDestroy/beforeUnmount. Those watchers often capture large reactive objects through closures.
Angular: subscriptions that never unsubscribe
RxJS subscriptions are another example of common causes of memory leaks in JavaScript:
this.userService.user$.subscribe(user => {
this.user = user;
});
Without takeUntil, async pipe, or manual unsubscribe in ngOnDestroy, the subscription can keep the component instance alive.
Across frameworks, the theme is the same: lifecycle hooks exist for a reason. If you ignore them, you create long-lived references that prevent garbage collection.
How to recognize these examples of memory leaks in practice
Knowing theory is nice, but you need a workflow. Here’s how teams in 2024–2025 typically spot these examples of common causes of memory leaks in JavaScript:
Chrome DevTools (front end): Use the Performance and Memory panels to record a session, take heap snapshots, and look for:
- Detached DOM nodes
- Growing arrays or Maps
- Listeners attached to nodes that should be gone
Node.js (back end): Use
--inspectwith Chrome DevTools, or profiling tools like Clinic.js. Watch heap usage over time and take multiple snapshots. Look for long-lived objects that keep growing.Monitoring: Track process memory in production with tools like Prometheus + Grafana, Datadog, or New Relic. If memory looks like a staircase (up, flat, up, flat) rather than a wave (up, down, up, down), you likely have a leak.
For background on how garbage collection works in managed runtimes, resources from universities such as MIT OpenCourseWare and Stanford CS courses provide solid fundamentals on memory management and GC strategies, even if they’re not JavaScript-specific.
FAQ: short answers and more examples
What are some real examples of common causes of memory leaks in JavaScript?
Real examples include:
- Event listeners on DOM nodes that are never removed.
setIntervalorsetTimeoutcallbacks that are never cleared.- Detached DOM nodes kept in arrays or maps.
- Global caches or singletons that grow without eviction.
- Node.js
EventEmitterlisteners that are never unsubscribed. - Framework-specific issues like React effects without cleanups or Angular subscriptions without
unsubscribe.
How do I know if my JavaScript app has a memory leak?
Watch memory usage over time. In the browser, use Chrome DevTools to take repeated heap snapshots while interacting with the app. In Node.js, monitor process memory and use the inspector. If memory grows steadily under a stable workload and doesn’t return to a baseline, you likely have one of the examples of common causes of memory leaks in JavaScript described above.
Can closures alone cause leaks in modern JavaScript?
Closures by themselves are not a problem; they’re just a feature. They become a problem when they’re tied to long-lived references like intervals, global arrays, or event listeners. The closure then keeps its entire scope alive. That’s why timer callbacks and listeners are such a frequent example of closure-based leaks.
Are memory leaks still a big issue with modern frameworks in 2024–2025?
Yes. Frameworks hide some complexity, but they can’t protect you from every bad pattern. Long-lived SPAs, real-time dashboards, and Node.js services running for weeks make leaks more visible, not less. The examples of common causes of memory leaks in JavaScript haven’t disappeared; they’ve just moved into framework lifecycles and abstractions.
What’s the best example of a simple fix that prevents many leaks?
Adding systematic cleanup: always pair setup with teardown. For every addEventListener, there should be a removeEventListener. For every setInterval, there should be a clearInterval. For every subscription, there should be an unsubscribe. That simple discipline eliminates a surprisingly large share of the real examples you see in production.
If you treat these examples of common causes of memory leaks in JavaScript as a checklist—event listeners, timers, detached DOM, global state, caches, and subscriptions—you’ll catch most leaks long before your users feel them.
Related Topics
Explore More Memory Leaks
Discover more examples and insights in this category.
View All Memory Leaks