Reducing EF Core Memory Overhead with AsNoTracking
Learn how to optimize EF Core performance by using AsNoTracking to reduce memory overhead and CPU usage in read-only scenarios.
27 Apr 2026, 19:58 UTC

The Hidden Cost of Entity Tracking
When you query data using Entity Framework Core, the framework doesn't just fetch the rows; it creates a snapshot of every entity in the ChangeTracker. This process allows EF Core to detect changes to your objects and persist them back to the database when you call SaveChanges(). While powerful, this tracking mechanism consumes significant CPU and memory, especially during high-volume read operations where you have no intention of updating the data.
The takeaway is simple: if you are fetching data for a read-only view, a report, or a JSON API response, tracking is wasted effort. Using AsNoTracking() tells EF Core to bypass the snapshot process, resulting in faster execution and a smaller memory footprint.
How AsNoTracking Changes Execution
By default, EF Core performs "Identity Resolution." If a query returns the same database row multiple times, EF Core ensures that only one instance of that object exists in memory. To do this, it must check the ChangeTracker for every single row returned.
When you apply AsNoTracking(), EF Core skips the identity map and the snapshotting phase. It simply materializes the objects from the database reader and hands them to you. This is particularly effective for large result sets or complex joins where the number of tracked entities could otherwise grow into the thousands during a single request.
Implementation Example
Consider a scenario where an API endpoint needs to list all active products. Since the API only returns a list to the client, there is no need to track these entities for updates.
// Run this within your Service or Controller layer
// Required Permissions: Read access to the database
public async Task<List<ProductDto>> GetActiveProductsAsync(ApplicationDbContext dbContext)
{
// .AsNoTracking() is called on the IQueryable before the query is executed
var products = await dbContext.Products
.AsNoTracking()
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
return products;
}
Expected Result: The query executes faster, and the DbContext instance remains "lean" because it isn't holding references to every product returned.
The Trade-off: Read-Only vs. Read-Write
The primary limitation of AsNoTracking() is that the returned entities are "detached." If you modify a property on a non-tracked entity and call SaveChanges(), EF Core will not detect the change, and nothing will be updated in the database.
| Feature | Tracked (Default) | AsNoTracking |
|---|---|---|
| Memory Usage | Higher (Snapshots stored) | Lower (No snapshots) |
| Execution Speed | Slower (Identity checks) | Faster (Direct materialization) |
| Update Ability | Automatic via SaveChanges() | Requires manual Re-Attach |
Verifying the Performance Gain
To confirm that tracking is actually disabled, you can inspect the ChangeTracker immediately after your query. Run the following check in a debug session or a unit test:
var products = await dbContext.Products.AsNoTracking().ToListAsync();
// This should return 0 if AsNoTracking worked correctly
int trackedCount = dbContext.ChangeTracker.Entries().Count();
Console.WriteLine($"Entities currently tracked: {trackedCount}");
For a quantitative measure, use a benchmarking tool like BenchmarkDotNet to compare the Mean execution time and Gen 0/1/2 garbage collection allocations between a tracked query and a non-tracked query on a dataset of 1,000+ records.
Practical Decision Guide
To decide which approach to use, ask these two questions:
- Will I call
SaveChanges()on these specific objects in this request? If no, useAsNoTracking(). - Is this a high-traffic read endpoint? If yes,
AsNoTracking()is mandatory to prevent memory pressure and excessive GC pauses.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.