I’ll give you:
- One self-contained Program.cs you can drop into a new project
- How to run it from command line
- Step-by-step: how to use dotMemory to detect the leak
- How to read dotMemory results & locate the leak
- How to fix it (clean version of the code)
1️⃣ Create the demo app
dotnet new web -n MemoryLeakDemo
cd MemoryLeakDemo
Code language: JavaScript (javascript)
Now replace the entire Program.cs with this:
This is self-sufficient: one file, no extra classes, no extra projects.
2️⃣ Run the app (command line)
From the MemoryLeakDemo folder:
dotnet run
It will listen on something like:
http://localhost:5000https://localhost:5001
(Use the HTTP one for simplicity: http://localhost:5000)
3️⃣ Generate the leak
In another terminal (or browser / Postman):
A. Start leaking memory
Run a loop (PowerShell):
1..50 | ForEach-Object {
curl "http://localhost:5000/leak"
}
Code language: JavaScript (javascript)
Or Git Bash:
for i in {1..50}; do curl -s "http://localhost:5000/leak" > /dev/null; done
Code language: JavaScript (javascript)
Then check stats:
curl "http://localhost:5000/stats"
Code language: JavaScript (javascript)
You should see Total stored MB growing (e.g. 250 MB).
B. Compare with non-leaking allocations
1..50 | ForEach-Object {
curl "http://localhost:5000/noleak"
}
curl "http://localhost:5000/gc"
curl "http://localhost:5000/stats"
Code language: JavaScript (javascript)
/noleakallocates memory but does not store it → GC can clean it./leakallocates memory and keeps it in a static list → cannot be GC’d.
4️⃣ Profile with dotMemory – step by step
Assuming you have dotMemory GUI (Rider/Standalone):
Step 1: Start the app
Make sure dotnet run for MemoryLeakDemo is running.
Step 2: Open dotMemory
- Start dotMemory
- Choose “Profile Running Process” (or similar wording)
- Select the
dotnet.exe/MemoryLeakDemo.dllprocess - Click Run / Attach
Step 3: Take a baseline snapshot
In dotMemory:
- Click “Get Snapshot” (or “Get Snapshot #1”).
- Name it “Baseline (before leak)”.
Step 4: Generate the leak
Back in terminal:
1..100 | ForEach-Object {
curl "http://localhost:5000/leak"
}
curl "http://localhost:5000/stats"
Code language: JavaScript (javascript)
You should see big total MB stored.
Step 5: Take snapshot after leak
In dotMemory:
- Click “Get Snapshot” again.
- Name it “After leak (static list)”.
Step 6: Optionally, force GC and snapshot
Back in terminal:
curl "http://localhost:5000/gc"
curl "http://localhost:5000/stats"
Code language: JavaScript (javascript)
Then in dotMemory:
- Take Snapshot #3: “After GC”.
5️⃣ Understanding dotMemory results (how to spot the leak)
Now the fun part: reading dotMemory.
A. Compare snapshots (Baseline vs After leak)
- In dotMemory, select “Baseline” and “After leak” snapshots.
- Click “Compare Snapshots”.
Look at:
- Heap size increased a lot
- New objects count is huge
- Top types by size will likely show
System.Byte[](byte arrays)
B. Drill into byte[] and find retention
Click on System.Byte[] and open:
- “Retention Graph”
- or “Shortest Paths to GC Roots”
You should see a path like:
Static field → LeakStore.Buffers → List<byte[]> → byte[]
Code language: CSS (css)
This tells you:
- The
byte[]objects are alive because they are referenced by a static list. - That static list is
LeakStore.Buffers.
This is your memory leak.
C. Check what happens after GC
Compare “After leak” vs “After GC” snapshots:
- If
byte[]objects are still around in large numbers/size → GC cannot free them. - Why? Because they’re still referenced by
LeakStore.Buffers.
That is exactly how dotMemory exposes real leaks:
Objects survive GCs and are retained by long-lived references (statics, singletons, caches, events, etc.).
6️⃣ Common real-world patterns similar to this leak
- Static lists/dictionaries that grow but never shrink
- Caches without eviction policies
- Event handlers where you forget to
-=unsubscribe - Long-lived singletons holding request-scoped data
TaskorTimercallbacks closing over large objects- In-memory queues that never get drained
dotMemory will show all of these as:
- Big retained size
- GC roots pointing to static fields, singletons, or event tables
7️⃣ How to fix this leak (clean code version)
Here’s a fixed version of the leak part:
Option A: Limit the cache
static class LeakStore
{
private const int MaxBuffers = 10;
private static readonly Queue<byte[]> Buffers = new();
public static void AddBuffer(byte[] buffer)
{
Buffers.Enqueue(buffer);
while (Buffers.Count > MaxBuffers)
{
Buffers.Dequeue(); // old buffers become eligible for GC
}
}
public static int Count => Buffers.Count;
}
Code language: PHP (php)
Then in /leak:
var buffer = new byte[bytes];
// ...
LeakStore.AddBuffer(buffer);
Code language: JavaScript (javascript)
Now dotMemory will show:
- Heap size not growing unbounded
- Byte arrays being collected over time
Option B: Don’t use static leaks at all
- Use scoped dependencies
- Avoid unbounded global collections
- Use proper cache with TTL / LRU (e.g., MemoryCache)
8️⃣ In-Code Metrics (like we did for TLS)
For this example, /stats already gives:
- Number of stored buffers
- Total MB stored
- Number of leak calls
You can show this in the terminal while dotMemory shows heap graph growth → this connects code behavior + profiler view.
I’m a DevOps/SRE/DevSecOps/Cloud Expert passionate about sharing knowledge and experiences. I have worked at Cotocus. I share tech blog at DevOps School, travel stories at Holiday Landmark, stock market tips at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow , and SEO strategies at Wizbrand.
Do you want to learn Quantum Computing?
Please find my social handles as below;
Rajesh Kumar Personal Website
Rajesh Kumar at YOUTUBE
Rajesh Kumar at INSTAGRAM
Rajesh Kumar at X
Rajesh Kumar at FACEBOOK
Rajesh Kumar at LINKEDIN
Rajesh Kumar at WIZBRAND