
Modern .NET and ASP.NET Core applications are designed with automatic memory management using the Garbage Collector (GC). This feature significantly reduces manual memory handling compared to languages like C or C++.
However, memory leaks can still occur in .NET applications when objects remain referenced unintentionally. Over time, these leaks can cause high memory usage, degraded performance, slow APIs, and even application crashes.
Memory leaks are especially dangerous in long-running applications such as:
- ASP.NET Core Web APIs
- Background workers
- Microservices
- Windows services
- Cloud-hosted applications
In this guide, you will learn:
- What memory leaks are in .NET Core
- Common causes of memory leaks
- How to detect memory leaks
- Tools used by professionals
- Practical code examples to prevent memory leaks
This guide focuses on real-world production troubleshooting techniques used by experienced .NET developers.
What is a Memory Leak in .NET?
A memory leak happens when an application allocates memory but does not release it when it is no longer needed.
In .NET, the Garbage Collector (GC) automatically frees unused objects. However, if objects remain referenced in memory, the GC cannot remove them.
This results in:
- Continuous memory growth
- Higher CPU usage due to frequent GC cycles
- Reduced application performance
- Possible application crashes
Example symptoms include:
- ASP.NET Core API memory continuously increasing
- Containers restarting due to memory limits
- Slow response times under heavy load
Why Memory Leaks Still Occur in .NET Core
Even with automatic memory management, developers can accidentally create situations where objects cannot be collected.
Common causes include:
- Unreleased event subscriptions
- Static object references
- Improper caching
- Not disposing unmanaged resources
- Long-lived services holding references
- Background tasks retaining objects
Understanding these patterns is the first step toward solving memory problems.
How Garbage Collection Works in .NET
The .NET Garbage Collector manages memory using a generational model.
Objects are categorized into three generations.
Generation 0
Contains short-lived objects.
Example:
- Temporary variables
- Request objects
These objects are collected frequently.
Generation 1
Objects that survive Generation 0 collections move here.
Generation 2
Long-lived objects end up in Generation 2.
Examples include:
- Cached data
- Static objects
- Application-level services
Memory leaks usually appear as growing Generation 2 memory usage.
Common Causes of Memory Leaks in .NET Core Applications
Let’s explore real coding scenarios that cause memory leaks.
1 Event Handler Memory Leaks
One of the most common memory leak scenarios occurs when objects subscribe to events but never unsubscribe.
Example of a problematic implementation:
public class OrderService
{
public OrderService()
{
OrderProcessor.OrderCompleted += HandleOrderCompleted;
} private void HandleOrderCompleted(object sender, EventArgs e)
{
Console.WriteLine("Order completed");
}
}
If this service is created multiple times, each instance remains referenced by the event handler.
The garbage collector cannot release it.
Fix: Always Unsubscribe from Events
public class OrderService : IDisposable
{
public OrderService()
{
OrderProcessor.OrderCompleted += HandleOrderCompleted;
} private void HandleOrderCompleted(object sender, EventArgs e)
{
Console.WriteLine("Order completed");
} public void Dispose()
{
OrderProcessor.OrderCompleted -= HandleOrderCompleted;
}
}
Using IDisposable ensures proper cleanup.
2 Static Objects Holding References
Static objects live for the entire lifetime of the application.
If they reference large objects, memory may never be released.
Example:
public static class CacheManager
{
public static List<Customer> Customers = new List<Customer>();
}
If this list keeps growing, the memory will never be released.
Fix: Use MemoryCache with Expiration
Modern .NET applications should use IMemoryCache.
Example:
public class CustomerService
{
private readonly IMemoryCache _cache; public CustomerService(IMemoryCache cache)
{
_cache = cache;
} public List<Customer> GetCustomers()
{
return _cache.GetOrCreate("customers", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
return LoadCustomersFromDatabase();
});
}
}
This ensures the cache expires and memory is released.
3 Not Disposing Unmanaged Resources
Objects such as:
- File streams
- Database connections
- HTTP clients
- Network streams
use unmanaged resources and must be disposed.
Bad example:
public void ReadFile()
{
FileStream fs = new FileStream("data.txt", FileMode.Open);
}
This file handle remains open.
Correct Implementation Using using
public void ReadFile()
{
using FileStream fs = new FileStream("data.txt", FileMode.Open);
}
The using statement automatically disposes resources.
4 Long-Lived Background Services
Background services may unintentionally store objects in memory.
Example:
public class DataCollector
{
private static List<string> _logs = new(); public void AddLog(string log)
{
_logs.Add(log);
}
}
If logs keep increasing, memory will grow indefinitely.
Fix: Limit Stored Data
public void AddLog(string log)
{
if (_logs.Count > 1000)
_logs.RemoveAt(0); _logs.Add(log);
}
Or use a logging framework instead of storing logs manually.
Tools to Detect Memory Leaks in .NET Applications
Professional developers rely on specialized tools to analyze memory usage.
1 dotnet-counters
The dotnet-counters tool monitors runtime metrics.
Run the following command:
dotnet-counters monitor -p <process-id>
Important metrics include:
- GC Heap Size
- Allocation Rate
- Gen 2 GC Count
If heap size continuously increases, it may indicate a memory leak.
2 dotnet-dump
This tool captures memory snapshots of running applications.
Command:
dotnet-dump collect -p <process-id>
You can then analyze the dump using:
dotnet-dump analyze
This helps identify objects consuming the most memory.
3 Visual Studio Diagnostic Tools
Visual Studio provides built-in profiling tools.
Steps:
1 Open Debug → Performance Profiler
2 Select Memory Usage
3 Run the application
4 Capture memory snapshots
You can compare snapshots to identify growing objects.
4 JetBrains dotMemory
JetBrains dotMemory is one of the most powerful .NET memory profiling tools.
It helps developers:
- Identify object retention paths
- Detect memory leaks
- Analyze GC behavior
Monitoring Memory in Production
In production environments, monitoring tools are essential.
Popular monitoring platforms include:
- Azure Application Insights
- Prometheus
- Grafana
- Datadog
These tools help track:
- Heap size
- GC activity
- Memory usage trends
If memory steadily increases over time, a memory leak may exist.
Best Practices to Prevent Memory Leaks
Follow these practices to keep your .NET applications healthy.
Always Dispose Resources
Use using or implement IDisposable.
Avoid Static Memory Storage
Do not store large collections in static variables.
Use Proper Caching Strategies
Use IMemoryCache or Redis instead of custom caching.
Unsubscribe from Events
Always remove event handlers when objects are disposed.
Monitor Production Systems
Track memory usage regularly using monitoring tools.
Real-World Example of Memory Leak Detection
Imagine a high-traffic ASP.NET Core API that processes thousands of requests per minute.
After deployment, engineers notice:
- API memory increases continuously
- Containers restart frequently
Using dotnet-counters, they detect:
GC Heap Size increasing constantly
Using dotnet-dump, they identify a large number of objects held by an event handler.
After fixing the event subscription issue, memory stabilizes and the application runs normally.
This demonstrates the importance of monitoring and proper memory management.
Frequently Asked Questions (FAQ)
What is the most common cause of memory leaks in .NET?
The most common causes include unreleased event handlers, static object references, improper caching, and unmanaged resources not being disposed properly.
Does .NET automatically prevent memory leaks?
No. While the Garbage Collector manages memory automatically, developers must ensure objects are no longer referenced.
How can I check memory usage in a .NET application?
You can use tools such as:
- dotnet-counters
- dotnet-dump
- Visual Studio Profiler
- JetBrains dotMemory
These tools help analyze memory allocations and object references.
What is the Large Object Heap (LOH)?
The Large Object Heap stores objects larger than 85KB.
Frequent allocations in LOH can cause fragmentation and reduce application performance.
Can ASP.NET Core applications have memory leaks?
Yes. ASP.NET Core applications can suffer from memory leaks if objects are referenced unintentionally, especially in caching, background services, and event subscriptions.
Conclusion
Memory leaks in .NET applications can silently degrade system performance and lead to serious production issues. Even though .NET provides automatic memory management, developers must still follow best practices to ensure objects are released properly.
By understanding how the Garbage Collector works, identifying common leak patterns, and using tools such as dotnet-counters, dotnet-dump, and Visual Studio Profiler, developers can quickly detect and fix memory problems.
Regular monitoring, proper resource disposal, and careful architecture decisions will help keep your ASP.NET Core applications stable, scalable, and efficient.
More Important Articles
- SQL Server Deadlock Detection: How to Detect and Fix Deadlocks
- SQL Server Performance Tuning: Complete Guide with Real Examples
- Handling Large File Uploads in ASP.NET Core: Best Practices, Performance, and Security
- ASP.NET Core Swagger Not Showing – Complete Solution Guide
- Why IQueryable Is Dangerous in APIs (Real-World Risks, Examples, and Best Practices)
- Add Serilog Without Breaking Dependency Injection in ASP.NET Core
- ASP.NET Core API Performance: Proven Techniques to Build Fast, Scalable APIs
