Entity Framework  

EF Core Performance Benchmarks: 12 Mistakes That Hurt Query Speed

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:

MetricWhy It Matters
Query DurationOverall execution time
Logical ReadsIndicates I/O cost
CPU TimeProcessing overhead
Memory AllocationApplication efficiency
Network PayloadData transfer size
Rows ReturnedIdentifies 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.

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 QuerySplit Query
Fewer database round tripsMultiple SQL queries
Can produce Cartesian explosionPrevents duplicate result sets
Better for small datasetsBetter 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:

  • Faster response times

  • Consistent performance

  • Reduced database workload

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

SymptomLikely CauseRecommended Fix
High database CPUMissing indexesReview execution plans and add appropriate indexes
Excessive memory usageEntity trackingUse AsNoTracking() for read-only operations
Hundreds of SQL queriesLazy loadingReplace with projections or eager loading
Slow API responsesLarge payloadsSelect only required columns
Long-running paginationLarge Skip() valuesSwitch to keyset pagination
Duplicate rowsMultiple collection includesUse 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:

  1. Measure the slow query.

  2. Inspect the generated SQL.

  3. Review the execution plan.

  4. Add or optimize indexes.

  5. Reduce returned columns.

  6. Eliminate N+1 queries.

  7. Disable tracking where appropriate.

  8. Evaluate split queries.

  9. Consider compiled queries for hot paths.

  10. 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.