# Memory Leak Audit

> Audit JavaScript/TypeScript code for memory leaks and apply fixes. Use when the user asks to find, diagnose, fix, optimize, or prevent memory leaks, growing heap, high memory usage, or OOM crashes in Node.js, Next.js, Nuxt, browser, React, or Vue code.

- Skill: `mantvmass/memory-leak-audit` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add mantvmass/memory-leak-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mantvmass/memory-leak-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: mantvmass (https://skillmd.com/u/mantvmass)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mantvmass/memory-leak-audit

---


# 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:

1. Something keeps a reference to it (an array, a closure, a listener, a cache).
2. 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

1. **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.
2. **Scan for the 8 categories below.** For each, grep for the signature, then
   check whether the retained thing is ever released.
3. **Rank by severity** — `critical` (fast unbounded growth, process-killer),
   `high` (unbounded, OOM under load), `medium` (slow growth), `low` (subtle).
4. **Apply the smallest fix** that bounds or releases the memory (see below and
   `reference.md` for all 60 concrete patterns).
5. **Verify.** Server: sample `process.memoryUsage().heapUsed` over 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.

