Entity Framework Core (EF Core) simplifies database access by allowing developers to work with strongly typed entities instead of handwritten SQL. However, convenience comes at a cost when queries are not optimized. Applications that perform well during development can become significantly slower in production due to inefficient query patterns, excessive tracking, unnecessary data retrieval, and poor indexing.
Many developers attempt to improve performance by adding more CPU or scaling the application horizontally, while the real bottleneck is often a few inefficient EF Core queries.
This article examines 12 common EF Core mistakes that negatively impact query performance, explains why they occur, and provides production-ready solutions. Rather than relying on theoretical advice, the recommendations focus on measurable performance improvements that can be validated using BenchmarkDotNet, SQL Server Query Store, execution plans, and application telemetry.
Why EF Core Performance Matters
Every inefficient query affects more than response time.
Slow queries increase:
Database CPU utilization
Memory consumption
Network traffic
Lock contention
Request latency
Infrastructure costs
A single poorly designed query executed thousands of times per minute can become the primary bottleneck of an otherwise scalable application.
Benchmarking Before Optimization
Performance tuning should always begin with measurement.
Instead of assuming a query is slow, benchmark it using tools such as:
BenchmarkDotNet
SQL Server Query Store
SQL Execution Plans
MiniProfiler
OpenTelemetry
dotnet-counters
Useful metrics include:
| Metric | Why It Matters |
|---|---|
| Query Duration | Overall execution time |
| Logical Reads | Indicates I/O cost |
| CPU Time | Processing overhead |
| Memory Allocation | Application efficiency |
| Network Payload | Data transfer size |
| Rows Returned | Identifies over-fetching |
If benchmark data isn't available for your environment, capture these metrics before and after each optimization to validate its impact.
Mistake #1 – Loading Entire Tables
One of the most common performance issues is retrieving all records and filtering them in memory.
Poor implementation:
var products = await context.Products.ToListAsync();
var activeProducts = products
.Where(p => p.IsActive)
.ToList();
This loads every row from the database before applying the filter.
Better implementation:
var activeProducts = await context.Products
.Where(p => p.IsActive)
.ToListAsync();
Filtering in SQL significantly reduces memory usage, network traffic, and execution time.
Production Tip
Always let the database perform filtering, sorting, and aggregation whenever possible.
Mistake #2 – Returning Unnecessary Columns
Many APIs return complete entities even though clients require only a few fields.
Example:
var users = await context.Users.ToListAsync();
If the table contains 30 columns but the client needs only three, most of the transferred data is wasted.
Instead, project directly into DTOs.
var users = await context.Users
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
})
.ToListAsync();
Projection reduces:
SQL payload size
Serialization overhead
Memory allocation
API response size
For large datasets, this optimization often produces noticeable improvements.
Mistake #3 – Forgetting AsNoTracking()
By default, EF Core tracks every entity it retrieves.
Tracking is essential when updating entities but unnecessary for read-only operations.
Default query:
var orders = await context.Orders.ToListAsync();
Optimized version:
var orders = await context.Orders
.AsNoTracking()
.ToListAsync();
Why It Matters
Change tracking stores metadata for every entity.
When thousands of records are retrieved, tracking consumes additional memory and CPU resources.
Use AsNoTracking() for:
Reporting
Search APIs
Dashboards
Read-only endpoints
Analytics
Avoid it when entities will be modified and saved back to the database.
Mistake #4 – The N+1 Query Problem
Lazy loading can silently generate hundreds of additional SQL queries.
Example:
var customers = await context.Customers.ToListAsync();
foreach (var customer in customers)
{
Console.WriteLine(customer.Orders.Count);
}
If there are 500 customers, EF Core may execute:
One query for customers
500 additional queries for orders
This is known as the N+1 Query Problem.
A better approach uses eager loading.
var customers = await context.Customers
.Include(c => c.Orders)
.ToListAsync();
Even better, project only the required information.
var customers = await context.Customers
.Select(c => new
{
c.Name,
OrderCount = c.Orders.Count
})
.ToListAsync();
Projection often outperforms eager loading because only the required data is retrieved.
Mistake #5 – Missing Database Indexes
EF Core cannot compensate for missing indexes.
Even perfectly written LINQ queries become slow if SQL Server performs table scans.
Consider this query:
var product = await context.Products
.FirstOrDefaultAsync(p => p.Sku == sku);
Without an index on Sku, SQL Server may inspect every row.
Proper indexing dramatically reduces query execution time.
Indexes should exist on:
Frequently filtered columns
Foreign keys
Join columns
Frequently sorted fields
Remember that indexes improve read performance but increase write overhead, so create them based on actual query patterns rather than indexing every column.
Mistake #6 – Calling ToList() Too Early
LINQ queries are executed only when materialized.
This is efficient:
var products = await context.Products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name)
.Take(20)
.ToListAsync();
This is not:
var products = context.Products.ToList();
var result = products
.Where(p => p.Price > 100)
.Take(20);
Calling ToList() prematurely forces EF Core to load the entire dataset before applying filters.
A simple rule is to build the query first and materialize it only once at the end.
Performance Checklist
Before deploying an EF Core application, verify the following:
✅ Filter data in SQL, not in memory.
✅ Project only required columns.
✅ Use
AsNoTracking()for read-only queries.✅ Eliminate N+1 query patterns.
✅ Create indexes based on query workloads.
✅ Delay
ToListAsync()until the query is fully composed.
These six improvements alone often eliminate the majority of EF Core performance issues observed in production systems.
Mistake #7 – Executing Multiple Queries Instead of Batching
Many applications execute several independent queries for related data.
var customer = await context.Customers
.FirstAsync(c => c.Id == id);
var orders = await context.Orders
.Where(o => o.CustomerId == id)
.ToListAsync();
var invoices = await context.Invoices
.Where(i => i.CustomerId == id)
.ToListAsync();
Although each query is simple, every database round trip adds network latency. Under heavy load, these small delays accumulate and reduce overall throughput.
Where appropriate, combine related data into a single projection.
Join the conversation! Your thoughts help the community grow.