DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on

10 Hidden Memory Leaks in ASP.NET Core β€” From 2.1 GB to 380 MB (Real Production

10 Hidden Memory Leaks in ASP.NET Core β€” From 2.1 GB to 380 MB (Real Production Metrics)

πŸŽ₯ Prefer to watch? I cover this end-to-end in a 2-3 min video here: https://youtu.be/05UAzS9LCQQ

2.1 GB to 380 MB Without Changing Architecture

Mattrx β€” six ASP.NET Core 9 instances, no architectural changes β€” went from 2.1 gigabytes of working set per instance down to 380 megabytes. Eight OOM-driven restarts per week became zero. Tail latency p99 dropped from 820 milliseconds to 180. The cause: ten boring leaks.

Mattrx production β€” same code, same SQL:
  Working set      2.1 GB β†’ 380 MB    (βˆ’82%)
  OOM restarts     8 / week β†’ 0       (90 days)
  GC Gen2 p99      240 ms β†’ 35 ms
  % Time in GC     18% β†’ 4%
  p99 latency      820 ms β†’ 180 ms
  Cost saved       ~$420 / month  (P2v3 β†’ P1v3)
Enter fullscreen mode Exit fullscreen mode

The Mental Model β€” Leaks Are References You Forgot

A leak in .NET is not a runtime myth. The garbage collector cannot free an object as long as something reachable still references it. A leak is a reference you forgot β€” a singleton holding a scope, a static event, a cache without a limit. Find the root. Cut it. Memory recovers.

// "Leak" in .NET = unreachable from your intent,
// but still reachable from a GC ROOT.
//
// GC roots: static fields, running threads,
//           strong-rooted handles, the singleton graph.
//
// Fix the rooting. Memory follows.
Enter fullscreen mode Exit fullscreen mode

Leak #1 β€” Singleton Captures a Scoped DbContext

A singleton constructor takes a DbContext. DI compiles, the app boots, and the first DbContext lives forever β€” pinned by the singleton. Three hundred and forty megabytes per instance, gone. The fix: inject IServiceScopeFactory and create a fresh scope per call.

// LEAK β€” singleton captures a scoped DbContext
class CampaignCache(AppDb db) { /* singleton */ }

// FIX β€” resolve a fresh scope per call
class CampaignCache(IServiceScopeFactory scopes)
{
  public async Task RefreshAsync()
  {
    using var s = scopes.CreateScope();
    var db = s.ServiceProvider.GetRequiredService<AppDb>();
    /* use db here, then dispose with the scope */
  }
}
// βˆ’340 MB / instance.
Enter fullscreen mode Exit fullscreen mode

Leak #4 β€” EF Core Change-Tracker Bloat

A long-lived DbContext loads twenty thousand entities a day for a dashboard query. The change tracker hangs on to every one β€” three hundred and ten megabytes per instance. The fix is two words: AsNoTracking on read paths. Or just keep the DbContext scoped per request, the way ASP.NET Core wires it by default.

// LEAK β€” long-lived ctx, no AsNoTracking
var rows = await db.Campaigns
    .Include(c => c.Reports)
    .ToListAsync();   // 20k entities pinned

// FIX β€” read-only path stays read-only
var rows = await db.Campaigns.AsNoTracking()
    .Include(c => c.Reports)
    .ToListAsync();
// βˆ’310 MB / instance.
Enter fullscreen mode Exit fullscreen mode

Watch the walkthrough

Go deeper

The full written guide has every code sample and the production checklist:

https://prepstack.co.in/blog/hidden-memory-leaks-aspnet-core-applications-causes-fixes-production-metrics


Which part have you actually hit in production? Drop a war story in the comments β€” I read every one.

If this saved you time, hit the ❀️ and the bookmark β€” that's what tells DEV's algorithm to show it to more devs.

Top comments (0)