PromptForge Academy
debuggingmemory-leaksdotnetperformance

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

CauseWhy it leaks
Event handler subscription without unsubscribeThe 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 evictionEvery 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 intendedA 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 disposedUnmanaged 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 scenariosThread 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

"Memory keeps growing, why?" isn't enough to work with

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

Prompt: diagnose a leak from a GC root path
Existing Project Advanced — Debugging module
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.