Skip to content

Instantly share code, notes, and snippets.

@devops-school
Created November 26, 2025 06:52
Show Gist options
  • Select an option

  • Save devops-school/33e129696cb93456f6ee66409adefc9e to your computer and use it in GitHub Desktop.

Select an option

Save devops-school/33e129696cb93456f6ee66409adefc9e to your computer and use it in GitHub Desktop.
DOTNET: Understanding Memory leaks and Debugging using dotMemory
using System.Diagnostics;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// ===============================================
// INTENTIONAL MEMORY LEAK DEMO
// ===============================================
//
// We simulate a classic .NET memory leak using:
// - A static List<byte[]> that never gets cleared
// - Each call to /leak allocates and stores large arrays
//
// dotMemory will show:
// - Growing heap
// - Many byte[] instances
// - Retention via a static field
//
// Endpoints:
// GET /leak -> allocate and store memory (leaks)
// GET /noleak -> allocate but don't store (no leak)
// GET /stats -> show current totals
// GET /gc -> force GC to see if memory is freed
//
// ===============================================
app.MapGet("/", () =>
"MemoryLeakDemo running. Use /leak, /noleak, /stats, /gc endpoints.");
app.MapGet("/leak", () =>
{
// Allocate 5 MB per call (adjust if you want more/less pressure)
const int sizeInMb = 5;
int bytes = sizeInMb * 1024 * 1024;
var buffer = new byte[bytes];
// Touch the buffer so it really gets allocated
buffer[0] = 1;
buffer[bytes - 1] = 2;
LeakStore.Buffers.Add(buffer);
LeakStore.TotalAllocatedBytes += bytes;
LeakStore.LeakCount++;
return Results.Text(
$"Allocated & STORED {sizeInMb} MB. " +
$"Total stored: {LeakStore.Buffers.Count} buffers, ~{LeakStore.TotalAllocatedBytes / (1024 * 1024)} MB.");
});
app.MapGet("/noleak", () =>
{
// Same allocation size, but NOT stored anywhere.
const int sizeInMb = 5;
int bytes = sizeInMb * 1024 * 1024;
var buffer = new byte[bytes];
buffer[0] = 1;
buffer[bytes - 1] = 2;
// Not stored → eligible for GC
return Results.Text($"Allocated {sizeInMb} MB (NO LEAK) - buffer not stored.");
});
app.MapGet("/stats", () =>
{
var sb = new StringBuilder();
sb.AppendLine("=== MemoryLeakDemo Stats ===");
sb.AppendLine($"Stored buffers : {LeakStore.Buffers.Count}");
sb.AppendLine($"Total stored MB : {LeakStore.TotalAllocatedBytes / (1024 * 1024)}");
sb.AppendLine($"Leak calls count : {LeakStore.LeakCount}");
sb.AppendLine();
sb.AppendLine("Use /leak repeatedly to grow memory.");
sb.AppendLine("Use /noleak to generate non-leaking allocations.");
sb.AppendLine("Use /gc to force a GC and see what survives.");
return Results.Text(sb.ToString());
});
app.MapGet("/gc", () =>
{
long before = GC.GetTotalMemory(forceFullCollection: false);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
long after = GC.GetTotalMemory(forceFullCollection: true);
return Results.Text(
$"Forced GC. Before: {before / (1024 * 1024)} MB, After: {after / (1024 * 1024)} MB. " +
"Because of the static list, most leaked data will remain.");
});
app.Run();
// ===============================================
// LeakStore MUST come AFTER all top-level code
// ===============================================
static class LeakStore
{
// This is the "leak": static list that holds onto large arrays forever.
public static readonly List<byte[]> Buffers = new();
// Metrics for convenience
public static long TotalAllocatedBytes;
public static long LeakCount;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment