Introduction
When working with EF Core, it's easy to feel confident once the code works locally. The API returns data, there are no errors, and performance seems fine with a small database. Production is very different. Real data is larger, less predictable, and accessed concurrently by many users. Code that looked harmless during development can suddenly cause slow APIs, high memory usage, or even crashes.
These problems are not caused by syntax errors, but by how EF Core is used. In this article, we'll look at common logic and tracking mistakes that beginners often make—and how to fix them before they become production issues.
Logic and Tracking Mistakes
1. Using Any() the Wrong Way – The Yes/No Disaster
Checking whether a record exists is a very common requirement. Unfortunately, it is also one of the easiest places to introduce serious performance issues.A frequent mistake is loading all records into memory and then checking for existence in code.
The innocent code:
var users = await _context.Users.ToListAsync();
if (users.Any(u => u.Email == email))
{
// exists
}
At first glance, this looks reasonable. However, this code forces EF Core to fetch the entire Users table from the database. Only after loading all rows into memory does it check whether a matching email exists.
For large tables, this causes unnecessary memory usage and slow response times. The database is already optimized to answer existence checks efficiently.
The correct fix:
var exists = await _context.Users
.AnyAsync(u => u.Email == email);
This approach lets the database stop searching as soon as a match is found.
2. Using First() Instead of FirstOrDefault() – The Hidden Crash
Another mistake that often appears only in production is the incorrect use of First().
The code:
var user = _context.Users.First(u => u.Email == email);
This works perfectly during development, especially when test data always contains matching records. In production, however, data may be missing, deleted, or inconsistent.
When no matching record exists, First() throws an exception, leading to unexpected crashes.
The safer alternative is FirstOrDefault(), which returns null when no record is found.
The fix:
var user = _context.Users
.FirstOrDefault(u => u.Email == email);
This allows your application to handle missing data gracefully instead of crashing.
3. Forgetting AsNoTracking() – The Silent Performance Killer
EF Core tracks entities by default. While this is necessary for updates, it becomes a hidden performance cost for read-only operations.
Consider a simple API that only reads data:
The code:
var orders = await _context.Orders.ToListAsync();
Even though no updates are performed, EF Core still tracks every entity returned. This consumes extra memory and adds overhead, especially under high load. For read-only queries, tracking is unnecessary.
The fix:
var orders = await _context.Orders
.AsNoTracking()
.ToListAsync();
Disabling tracking makes queries faster and reduces memory usage.
Conclusion
The issues covered in this article are easy to miss because the code looks correct and works fine with small datasets. However, loading unnecessary data, making unsafe assumptions, and allowing EF Core to track entities when it isn't needed can all lead to serious production problems.
By using the right EF Core methods and understanding how queries behave, you can avoid slow APIs and unexpected crashes. Small changes—like using AnyAsync, choosing FirstOrDefault, and applying AsNoTracking—can make a big difference as your application scales.