A senior developer and Claude AI collaborated to diagnose and fix a memory leak in an ASP.NET application where RequestContext objects were being created but not torn down. The fix took ~2 hours of iterative debugging, not "ask AI and done." This document shows how AI assistance requires domain expertise to be effective.
ActiveContextCount kept climbing from 1 to 139+ without returning to baseline. Requests were being set up but not torn down, causing a memory/connection leak.
AI's first theory: The authentication pipeline was making unnecessary DB calls for static assets.
We added static asset detection to skip GetUserId() calls for CSS/JS/fonts. This was a valid optimization but didn't address the leak.
AI suggestion: Add logging to track context creation/teardown.
// In RequestContext.Setup()
var count = Interlocked.Increment(ref _activeContextCount);
Loggers.Info(LogType.FRAMEWORK, $"RequestContext.Setup: ActiveContextCount={count}, ContextID={contextID}");
// In RequestContext.Teardown()
var count = Interlocked.Decrement(ref _activeContextCount);
Loggers.Info(LogType.FRAMEWORK, $"RequestContext.Teardown: ActiveContextCount={count}, ContextID={ContextID}");The developer shared logs showing:
Teardown: ContextID=698575cce76c56350432ba10 (count 137)
Setup: ContextID=698575cce76c56350432ba10 (count 138)
Setup: ContextID=698575cde76c56350432ba2c (count 138)
AI's interpretation: "The logs are out of order. Two requests are happening concurrently."
Developer's correction: "The logs are out of order, but what I'm showing you is a clear case of the file being requested twice but teardown only once."
This was the critical insight - 2 Setups, 1 Teardown = Leak.
AI explored several theories:
- Multiple
SetupRequest()calls in Global.asax.cs (developer confirmed guards existed) - IIS static file handler bypassing ASP.NET (checked web.config -
runAllManagedModulesForAllRequests="true") - Client aborting requests before EndRequest fires
AI discovered two teardown paths:
ExecutingContext.Complete()inGlobal_EndRequestRequestContextTeardown.Dispose()viaDisposeOnPipelineCompleted
Developer's key context: "This dual teardown was a hack to try to fix the problem you are investigating. I suspected somehow Complete wasn't being called."
This told us the problem existed before the hack was added, and the hack wasn't working.
AI examined WebThreadStorageFactory:
public T Value {
get {
HttpContext httpContext = HttpContext.Current;
if (httpContext != null && httpContext.Items.Contains(name))
return (T)httpContext.Items[name];
return default(T); // Returns null if HttpContext.Current is null!
}
}The insight: If HttpContext.Current is null when DisposeOnPipelineCompleted runs (thread switching, async cleanup), the lookup fails and RequestContext.Current returns null.
Then in RequestContextTeardown.Dispose():
RequestContext.Current?.Teardown(); // null?.Teardown() = nothing happens!AI's solution: Capture HttpContext directly in RequestContextTeardown.
Developer's correction: "I still think we have an issue because Teardown accesses Current which is thread storage which goes to HttpContext... I think we need to capture RequestContext.Current, not HttpContext."
The developer recognized that even with a captured HttpContext, the Teardown() method itself was calling var current = Current; internally, which would fail the same way.
Two changes:
1. RequestContext.cs - Use this instead of Current, add teardown gate:
private bool _tornDown = false;
public void Teardown()
{
if (_tornDown)
return;
_tornDown = true;
// Changed from: var current = Current; current?._executionTimer.Stop();
// To:
_executionTimer.Stop();
OnRequestCompleted?.Invoke(this); // Use 'this', not 'current'
// ... rest uses 'this' throughout
}2. Global.asax.cs - Capture the instance directly:
private static URL SetupRequest()
{
if (!ExecutingContext.IsInitialized)
{
RequestContext.Setup(context, url, nestedContainer);
// Capture AFTER Setup creates it - direct reference, no HttpContext dependency
System.Web.HttpContext.Current.DisposeOnPipelineCompleted(
new RequestContextTeardown(RequestContext.Current));
}
return url;
}The AI couldn't know that:
- The dual teardown was already a hack attempting to fix this
HttpContext.Currentcan be null during pipeline cleanup- The codebase's specific threading model and async patterns
Each correction from the developer refined the search:
- "Keep logging" → Instrumentation was valuable
- "2 Setups, 1 Teardown" → Identified the exact symptom
- "Capture RequestContext, not HttpContext" → Pointed to the real fix
The AI could:
- Search the codebase quickly
- Trace code paths
- Generate hypotheses
- Write the fix once identified
The AI couldn't:
- Know which hypotheses were already tried
- Understand the historical context of "hack" code
- Recognize subtle ASP.NET threading behaviors without hints
The developer's role:
- Provided critical context ("this was a hack")
- Corrected wrong directions quickly
- Recognized the real issue from AI's analysis
- Validated the final approach
The AI's role:
- Explored the codebase systematically
- Generated multiple hypotheses
- Wrote instrumentation and fixes
- Documented the journey
HttpContext.Current can be null when DisposeOnPipelineCompleted runs due to ASP.NET's thread management. The thread storage lookup failed, RequestContext.Current returned null (cast to DummyContext), and Teardown() was never called on the actual context instance.
Fix: Capture the RequestContext instance directly when it's created, eliminating the dependency on HttpContext.Current during teardown.
Debugging session: ~2 hours Lines of code changed: ~30 Hypotheses explored: 6+ Developer corrections that redirected the investigation: 4