Memory Leak Audit
Find and fix memory leaks in JavaScript/TypeScript codebases. This skill is based on the 60 leak patterns catalogued by the Memory Leak Laboratory (js-leak-lab).
When to use this skill
Use it when the user:
- reports a process whose memory grows over time, gets OOM-killed, or restarts
- has a browser tab that gets slow/janky the longer it runs
- asks "why does this leak?", "is this a leak?", or for a memory audit/optimization
- adds long-lived state, timers, listeners, caches, or subscriptions and wants a check
What a leak actually is
A memory leak is memory that stays retained (reachable by the GC) but is never used again. Before flagging something, confirm both halves:
- Something keeps a reference to it (an array, a closure, a listener, a cache).
- That thing only ever grows, or outlives the data's usefulness.
A bounded cache or a fixed-size pool is not a leak. An array that only ever
gets .push()-ed is.
Audit method
- Identify the runtime. Server (Node/Next/Nuxt) leaks accumulate across requests in a long-lived process; client (browser/React/Vue) leaks accumulate across component mounts/unmounts and user navigation. The fix differs.
- Scan for the 8 categories below. For each, grep for the signature, then check whether the retained thing is ever released.
- Rank by severity —
critical(fast unbounded growth, process-killer),high(unbounded, OOM under load),medium(slow growth),low(subtle). - Apply the smallest fix that bounds or releases the memory (see below and
reference.mdfor all 60 concrete patterns). - Verify. Server: sample
process.memoryUsage().heapUsedover time. Browser:performance.memory.usedJSHeapSize(Chromium) or compare heap snapshots.
The 8 leak categories
1. Collections & buffers that only grow
Signature: an array / Map / Set / Buffer at module or instance scope that
only ever gets .push() / .set() / .add() — never .shift() / .delete() / cleared.
Fix: bound it — circular buffer, max-size with eviction, or don't retain at all.
2. Timers & intervals
Signature: setInterval / setTimeout / requestAnimationFrame whose handle is
discarded, or kept but never clear* / cancelAnimationFrame-ed. Every pending timer
pins its callback's entire closure.
Fix: keep the handle; clear it on teardown (unmount, request end, stop()).
3. Listeners & observers
Signature: addEventListener / emitter.on / new MutationObserver /
IntersectionObserver called repeatedly with no matching removeEventListener /
.off / .disconnect.
Fix: remove/disconnect on teardown; register once, not per-render or per-request.
4. Closures holding large scope
Signature: a long-lived function (event handler, memoized callback, stored closure) that closes over a large object it does not fully need. Fix: capture only the small value actually used; drop references when done.
5. Async — promises & callbacks
Signature: promises that never settle, resolve functions stashed in an array,
pub/sub callback registries with no unsubscribe.
Fix: always settle or time out promises; expose and call an unsubscribe.
6. Caching & retention
Signature: a cache — often module-scope, so it survives across every request in
Next.js/Nuxt — keyed by unbounded or dynamic values, with no eviction (or eviction
code with a broken condition).
Fix: bounded LRU with a correct eviction check; request-scoped state instead of
module-scope; WeakMap/WeakRef when keys are objects.
7. Detached DOM
Signature: DOM nodes or <iframe>s removed from the document but still
referenced by JavaScript — the whole subtree (and an iframe's JS realm) stays alive.
Fix: drop the JS references when removing nodes; bound any node pool.
8. Framework state & lifecycle
React signature: useEffect with no cleanup return; intervals/subscriptions
started in effects and never cleared; state arrays that only grow; context values or
trees retained in refs.
Vue signature: watch/watchEffect created outside setup scope (so it is not
auto-disposed); event-bus handlers not removed in onBeforeUnmount; module-scope
reactive() stores; unbounded <KeepAlive>.
Fix: return a cleanup from useEffect; create watchers inside setup or stop
them in onBeforeUnmount; bound caches, keep-alive sets, and state arrays.
Reporting findings
For each finding report:
file:line— where it is- category (1–8 above) and severity (low/medium/high/critical)
- why it leaks — what holds the reference and why it is never released
- the fix — show the corrected code; prefer the smallest change that bounds or releases the memory
If asked to fix, apply the fixes and then re-scan to confirm nothing was missed.
Full pattern catalogue
reference.md (next to this file) lists all 60 concrete patterns grouped by
category, each with its bad signature and standard fix. Load it when you need a
specific pattern or a more exhaustive scan.