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:
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:
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.
var customer = await context.Customers
.Select(c => new CustomerDetailsDto
{
Id = c.Id,
Name = c.Name,
Orders = c.Orders.Select(o => new OrderDto
{
Id = o.Id,
Total = o.Total
}).ToList()
})
.FirstAsync(c => c.Id == id);
Reducing unnecessary database round trips usually provides greater performance gains than optimizing business logic.
Mistake #8 – Ignoring Query Compilation
EF Core translates LINQ expressions into SQL before execution. Frequently executed queries pay this translation cost every time unless they're compiled.
For high-traffic endpoints, compiled queries can reduce CPU overhead.
private static readonly Func<AppDbContext, int, Task<Product?>> GetProduct =
EF.CompileAsyncQuery(
(AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
Usage:
var product = await GetProduct(context, id);
When to Use Compiled Queries
Compiled queries are beneficial when:
The same query executes thousands of times.
Query shape rarely changes.
CPU utilization is high because of query translation.
Avoid compiling dynamic queries that change based on user filters, as the benefit is minimal.
Mistake #9 – Fetching Large Collections with Include()
Using Include() on multiple collection navigations can generate large Cartesian products.
Example:
var customers = await context.Customers
.Include(c => c.Orders)
.Include(c => c.Addresses)
.ToListAsync();
If each customer has multiple orders and addresses, the generated SQL may return duplicate rows, dramatically increasing memory usage.
EF Core provides split queries to avoid this.
var customers = await context.Customers
.Include(c => c.Orders)
.Include(c => c.Addresses)
.AsSplitQuery()
.ToListAsync();
Single Query vs Split Query
| Single Query | Split Query |
|---|
| Fewer database round trips | Multiple SQL queries |
| Can produce Cartesian explosion | Prevents duplicate result sets |
| Better for small datasets | Better for large object graphs |
Always benchmark both approaches because the optimal choice depends on the size and shape of your data.
Mistake #10 – Using Offset Pagination on Large Tables
Traditional pagination uses Skip() and Take().
var products = await context.Products
.OrderBy(p => p.Id)
.Skip(page * pageSize)
.Take(pageSize)
.ToListAsync();
This works well for small datasets, but large offsets become increasingly expensive because the database must scan and discard rows before returning the requested page.
For APIs with millions of records, keyset (seek) pagination is usually more efficient.
var products = await context.Products
.Where(p => p.Id > lastProductId)
.OrderBy(p => p.Id)
.Take(20)
.ToListAsync();
Keyset pagination provides:
The trade-off is that users cannot jump directly to an arbitrary page number.
Mistake #11 – Updating Records One at a Time
Updating entities individually causes multiple SQL statements.
foreach (var product in products)
{
product.IsActive = false;
}
await context.SaveChangesAsync();
For large datasets, this approach is slow because EF Core tracks every modified entity.
For bulk updates, use EF Core's ExecuteUpdateAsync() (available in modern EF Core versions).
await context.Products
.Where(p => p.IsDiscontinued)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(
p => p.IsActive,
false));
Advantages include:
No entity tracking
Fewer memory allocations
Single SQL statement
Better scalability
Similarly, ExecuteDeleteAsync() is preferable to loading and deleting thousands of entities individually.
Mistake #12 – Not Inspecting Generated SQL
Many developers assume LINQ always generates optimal SQL.
It doesn't.
Always inspect generated SQL during performance tuning.
var sql = context.Products
.Where(p => p.Price > 100)
.ToQueryString();
Console.WriteLine(sql);
Reviewing SQL helps identify:
Missing filters
Unnecessary joins
Cartesian products
Unexpected sorting
Poor query translation
Understanding the generated SQL is often the fastest way to discover hidden performance issues.
Troubleshooting Guide
| Symptom | Likely Cause | Recommended Fix |
|---|
| High database CPU | Missing indexes | Review execution plans and add appropriate indexes |
| Excessive memory usage | Entity tracking | Use AsNoTracking() for read-only operations |
| Hundreds of SQL queries | Lazy loading | Replace with projections or eager loading |
| Slow API responses | Large payloads | Select only required columns |
| Long-running pagination | Large Skip() values | Switch to keyset pagination |
| Duplicate rows | Multiple collection includes | Use AsSplitQuery() or redesign the query |
Production Best Practices
Profile SQL before optimizing C# code.
Project DTOs instead of returning entities.
Keep DbContext instances short-lived.
Enable detailed logging only during diagnostics.
Use compiled queries only for frequently executed query patterns.
Benchmark every optimization before adopting it.
Review execution plans regularly in production.
Monitor query duration using OpenTelemetry or Application Insights.
Common Anti-Patterns
Avoid these practices in production applications:
Returning complete entities from every endpoint.
Calling ToList() before filtering.
Relying on lazy loading in APIs.
Ignoring generated SQL.
Loading thousands of rows into memory for updates.
Assuming LINQ always produces efficient SQL.
Adding indexes without analyzing workload patterns.
Performance problems are rarely caused by EF Core itself. They typically arise from inefficient query design and lack of measurement.
Performance Optimization Priority
When optimizing an EF Core application, address issues in this order:
Measure the slow query.
Inspect the generated SQL.
Review the execution plan.
Add or optimize indexes.
Reduce returned columns.
Eliminate N+1 queries.
Disable tracking where appropriate.
Evaluate split queries.
Consider compiled queries for hot paths.
Benchmark the improvements.
Following this sequence prevents premature optimization and focuses effort where it delivers the greatest benefit.
FAQ
Is raw SQL always faster than EF Core?
Not necessarily. Well-written LINQ often generates highly optimized SQL. Raw SQL is most useful for complex reporting queries, vendor-specific features, or scenarios where LINQ cannot express the desired query efficiently.
Should every read query use AsNoTracking()?
Use it whenever the retrieved entities won't be modified. For update scenarios, tracking remains necessary.
Are compiled queries required for every application?
No. They provide measurable benefits primarily for high-throughput applications that repeatedly execute the same query shape.
Should I replace EF Core with Dapper for performance?
Only after benchmarking. Many applications achieve excellent performance with EF Core once query design, indexing, and projections are optimized. Switching ORMs without addressing inefficient SQL rarely solves the underlying problem.
Conclusion
EF Core performance depends far more on how queries are written than on the framework itself. Most production bottlenecks stem from loading excessive data, tracking unnecessary entities, inefficient pagination, missing indexes, and hidden N+1 query patterns—not from EF Core's abstractions.
Rather than optimizing blindly, adopt a measurement-first approach. Profile slow queries, inspect the generated SQL, review execution plans, and validate every change with benchmarks. Small improvements—such as using projections, AsNoTracking(), split queries, and bulk operations—can dramatically reduce CPU usage, memory consumption, and response times without sacrificing the productivity benefits that EF Core provides.
By combining sound query design with continuous monitoring and evidence-based optimization, you can build EF Core applications that remain fast, scalable, and reliable even as data volume and traffic continue to grow.