Memory Leak Analysis: Debugging .NET Memory Issues with AI
A managed runtime doesn't stop memory leaks — it just changes what causes them. Event handlers, static caches, and captured closures are the usual suspects. How to diagnose them from a memory dump with AI as a reasoning partner.
".NET has garbage collection, so it can't leak memory" is a common and wrong assumption. The GC reliably frees objects with no remaining references — the leak is that something is still referencing the object it shouldn't be. Memory leaks in a managed runtime are reference leaks, not allocation leaks, and diagnosing them means finding the unexpected reference, not the allocation.
The usual suspects
| Cause | Why it leaks |
|---|---|
| Event handler subscription without unsubscribe | The publisher holds a reference to the subscriber for as long as the publisher lives — if the publisher outlives the subscriber's intended lifetime (a static event, a long-lived service), the subscriber can never be collected |
| Static or singleton-scoped cache with no eviction | Every entry added lives for the application's lifetime by design — unbounded growth is a leak even though each reference is intentional |
| Closures capturing more than intended | A lambda or local function captures an enclosing variable (often `this`) that keeps a much larger object graph alive than the closure actually needs |
| IDisposable objects not disposed | Unmanaged resources (file handles, DB connections, unmanaged memory) held by a disposable object aren't freed by the GC alone — Dispose must be called |
| ThreadLocal / AsyncLocal without cleanup in pooled-thread scenarios | Thread pool threads are reused; values stored per-thread can accumulate across many logical operations that share the same physical thread |
The evidence that makes analysis real, not guesswork
A growing working set alone doesn't identify the leak — it could be legitimate caching, GC not having run yet, or an actual leak. The evidence that turns this into a diagnosable problem is a memory dump with a retention path: what object is growing in count, and what's the chain of references keeping each instance alive.
- Two memory dumps taken minutes apart under steady load, so you can diff which object types are growing in count (not just total bytes).
- For the growing type, the GC root path — the exact chain of references from a root (static field, thread stack, GC handle) to the leaked instances.
- Whether growth is linear with request/operation count (points to a per-operation leak, like an unremoved event handler) or a step function (points to a one-time cache that never evicts).
Worked example
Context: Memory dump analysis (dotnet-dump / WinDbg with SOS) shows 40,000 live instances of OrderNotifier, growing linearly with the number of orders processed since startup. GC root path for a sample instance:
0:000> !gcroot 0x1a2b3c4d
Static field EventBus.OrderPlaced
-> EventBus.OrderPlaced (delegate list)
-> OrderNotifier.HandleOrderPlaced
-> OrderNotifier instance
Here is the OrderNotifier constructor and the code that subscribes it to EventBus.OrderPlaced: [paste code]
Task: Explain the leak based on this exact root path, then propose the minimal fix.
Constraints:
- Base the explanation only on the root path and code shown — don't speculate about other possible leaks not evidenced here.
- The fix must address the root cause (the subscription lifecycle), not just null out a field, which wouldn't change GC behavior.
- Note whether OrderNotifier should be Disposable and unsubscribe in Dispose, or whether the subscription itself should use a weak reference pattern — state which fits this specific lifetime mismatch and why.Fix, then verify under the same load
As with any performance bug, the fix isn't confirmed by the explanation being plausible — it's confirmed by re-running the same steady-load scenario that exposed the leak and observing the object count stop growing. Treat the AI-proposed root cause as a hypothesis to verify against a second dump, not a conclusion to ship on faith.
This sits alongside deadlock investigation as one of the debugging categories that most rewards structured evidence gathering before asking for an explanation — the Debugging module treats both together as "evidence-first" debugging, distinct from the simpler stack-trace-to-fix cases that make up most day-to-day bugs.