Back to Blog
EF CoreEntity FrameworkLINQ.NETASP.NET CorePerformanceSQLBackend EngineeringSystem DesignDatabase Optimization

EF Core Performance Mistakes That Quietly Destroy Production APIs

EF Core Performance Mistakes That Quietly Destroy Production APIs

1. Introduction: Why EF Core Gets Blamed for Performance Problems

One of the most common opinions in backend engineering discussions is:

"EF Core is slow."

In reality, EF Core is usually not the problem.

Most performance issues come from developers not understanding what happens after LINQ gets translated into SQL.

That distinction matters.

A query that works perfectly against a local database with 200 rows can become catastrophic in production when:

  • tables contain millions of records
  • APIs receive concurrent traffic
  • joins become expensive
  • network latency increases
  • unnecessary data gets materialized

ORMs amplify engineering decisions.

Good query design produces highly productive systems.

Poor query design silently creates:

  • N+1 query explosions
  • excessive memory allocations
  • unnecessary database roundtrips
  • table scans
  • massive payload transfers
  • connection pool exhaustion

The deeper lesson is this:

Using an ORM does not remove the need to understand databases.

Senior backend engineers eventually realize that high-performance EF Core development is mostly about:

  • understanding SQL generation
  • minimizing data movement
  • shaping queries correctly
  • leveraging indexes effectively
  • avoiding unnecessary tracking and materialization

This article explores some of the most important EF Core performance practices every backend engineer should understand.

2. Understand This First: LINQ Is Not Running In Memory

One of the most important mental model shifts in EF Core is understanding that LINQ queries against DbSet<T> are not immediately executed in memory.

Consider this:

C#
var users = context.Users
    .Where(x => x.IsActive)
    .Select(x => x.Email);

Many developers subconsciously think this behaves like a normal in-memory C# collection.

It does not.

This query builds an expression tree that EF Core later translates into SQL.

The generated SQL may look like:

sql
SELECT Email
FROM Users
WHERE IsActive = 1;

This distinction is critical because EF Core query performance depends heavily on what SQL gets generated.

IQueryable vs IEnumerable

IQueryable<T>means:

  • query composition
  • deferred execution
  • SQL translation

IEnumerable<T> means:

  • in-memory iteration
  • LINQ-to-Objects
  • already materialized data

A common mistake is accidentally forcing materialization too early.

Example:

C#
var users = await context.Users.ToListAsync();

var activeUsers = users
    .Where(x => x.IsActive);

This loads the entire table into memory before filtering.

The correct approach:

C#
var activeUsers = await context.Users
    .Where(x => x.IsActive)
    .ToListAsync();

This pushes filtering to the database engine where it belongs.

3. The N+1 Query Problem

The N+1 query problem remains one of the most common production performance disasters in ORM-based systems.

Consider this:

C#
var orders = await context.Orders.ToListAsync();

foreach (var order in orders)
{
    Console.WriteLine(order.Customer.Name);
}

If lazy loading is enabled, EF Core may execute:

  • 1 query to load orders
  • N additional queries for customers

This becomes catastrophic under scale.

Instead of executing one optimized join query, the application creates dozens or hundreds of database roundtrips.

Why This Is Dangerous

Database latency compounds quickly.

Even if each query only takes 20ms:

  • 1 query = 20ms
  • 100 queries = 2 seconds

Cloud infrastructure magnifies this issue because every roundtrip crosses the network.

Better Approaches

i) Use Projection

C#
var orders = await context.Orders
    .Select(x => new
    {
        x.Id,
        CustomerName = x.Customer.Name
    })
    .ToListAsync();

ii) Use Include Carefully

C#
var orders = await context.Orders
    .Include(x => x.Customer)
    .ToListAsync();

Projection is often preferable because it fetches only the required data.

4. Stop Fetching Entire Entities

One of the highest-impact EF Core optimizations is reducing unnecessary data retrieval.

Bad:

C#
var users = await context.Users.ToListAsync();

This retrieves every mapped column.

Even if the API only needs:

  • Id
  • Name
  • Email

EF Core still materializes the entire entity.

This increases:

  • network transfer size
  • memory allocations
  • materialization overhead
  • serialization costs

Use Projection Instead

C#
var users = await context.Users
    .Select(x => new UserDto
    {
        Id = x.Id,
        Name = x.Name,
        Email = x.Email
    })
    .ToListAsync();

Projection dramatically improves performance because:

  • fewer columns are selected
  • less memory is allocated
  • payloads become smaller
  • indexes become more effective

This becomes especially important in:

  • high-throughput APIs
  • mobile backends
  • microservices
  • reporting endpoints

5. Use AsNoTracking for Read Queries

By default, EF Core tracks entities returned from queries.

This enables:

  • change detection
  • updates
  • relationship fixups

However, tracking introduces overhead.

For read-only queries, this overhead is often unnecessary.

i) Default Tracking Query

C#
var users = await context.Users
    .ToListAsync();

ii) No-Tracking Query

C#
var users = await context.Users
    .AsNoTracking()
    .ToListAsync();

iii) Why This Improves Performance

AsNoTracking() reduces:

  • memory usage
  • CPU overhead
  • change tracker allocations

This becomes very important for:

  • dashboards
  • analytics APIs
  • reporting systems
  • public APIs
  • high-read systems

iv) Important Trade-Off

Do not use AsNoTracking() if entities will later be modified and persisted.

This optimization is best suited for read-heavy workflows and CQRS-style query models.

6. Premature ToList() Calls

One of the easiest ways to accidentally destroy query performance is calling ToList() too early.

Bad:

C#
var users = await context.Users.ToListAsync();

var filtered = users
    .Where(x => x.IsActive)
    .OrderBy(x => x.Name);

This loads the entire dataset into memory before filtering.

The database engine is far more optimized for filtering and sorting than application memory.

i) Correct Approach

C#
var filtered = await context.Users
    .Where(x => x.IsActive)
    .OrderBy(x => x.Name)
    .ToListAsync();

ii) The Senior Engineering Perspective

Backend performance is often about minimizing data movement.

Every unnecessary row transferred:

  • consumes memory
  • increases network cost
  • slows serialization
  • reduces throughput

Databases are optimized for set-based operations.

Applications should leverage that instead of reimplementing filtering in memory.

7. Learn to Read Generated SQL

One of the biggest differences between intermediate and senior EF Core developers is the ability to inspect generated SQL.

EF Core is an abstraction layer.

But abstractions do not eliminate underlying system behavior.

i) Inspecting Generated SQL

C#
var query = context.Users
    .Where(x => x.IsActive);

Console.WriteLine(query.ToQueryString());

This is an incredibly valuable debugging tool.

ii) Why SQL Inspection Matters

Generated SQL reveals:

  • unnecessary joins
  • missing filters
  • table scans
  • poor query shape
  • cartesian explosions
  • inefficient ordering

iii) Learn Execution Plans

Eventually, backend engineers must become comfortable reading:

  • SQL execution plans
  • index usage
  • scans vs seeks
  • estimated costs

Many EF Core performance problems are actually database design problems.

ORM knowledge alone is not enough.

Strong database fundamentals matter just as much.

8. Indexes Matter More Than LINQ Tricks

Many developers spend time micro-optimizing LINQ syntax while ignoring indexes entirely.

In practice, indexes often have a much larger impact than ORM-level optimizations.

Example

C#
var users = await context.Users
    .Where(x => x.Email == email)
    .FirstOrDefaultAsync();

Without an index on Email, the database may perform a full table scan.

At scale, this becomes extremely expensive.

i) Important Indexing Concepts

a) Composite Indexes

Useful for queries filtering multiple columns.

b) Covering Indexes

Allow queries to retrieve all required data directly from the index.

c) Ordering Alignment

Indexes can dramatically improve sorting performance.

ii) Key Lesson

EF Core generates SQL.

The database engine still determines execution performance.

Understanding indexing strategy is one of the highest ROI backend engineering skills.

9. Split Queries vs Cartesian Explosion

Large Include() chains can silently create massive result sets.

Example:

C#
var orders = await context.Orders
    .Include(x => x.Items)
    .Include(x => x.Payments)
    .ToListAsync();

This may generate a large join producing duplicated rows.

This problem is known as cartesian explosion.

i) Using Split Queries

C#
var orders = await context.Orders
    .Include(x => x.Items)
    .Include(x => x.Payments)
    .AsSplitQuery()
    .ToListAsync();

This breaks the query into multiple SQL queries.

ii) Trade-Offs

a) Single Query

Pros:

  • fewer roundtrips

Cons:

  • duplicated rows
  • larger payloads
  • memory pressure

b) Split Query

Pros:

  • smaller payloads
  • reduced duplication

Cons:

  • additional roundtrips

The correct choice depends on:

  • relationship size
  • network latency
  • payload volume
  • query frequency

10. Compiled Queries for Hot Paths

EF Core compiles LINQ queries internally before execution.

For most applications, this overhead is negligible.

However, extremely high-frequency queries can benefit from compiled queries.

Example:

C#
private static readonly Func<AppDbContext, string, Task<User?>> GetUserByEmail =
    EF.CompileAsyncQuery(
        (AppDbContext context, string email) =>
            context.Users.FirstOrDefault(x => x.Email == email)
    );

i) When This Helps

Compiled queries are useful for:

  • hot API endpoints
  • frequently executed queries
  • low-latency systems
  • high-throughput services

ii) Important Caveat

Compiled queries are not magic.

Poor SQL, missing indexes, or excessive payload sizes will still dominate performance costs.

Always optimize the highest-impact bottlenecks first.

11. Use Bulk Operations Instead of Entity Loops

Many applications accidentally perform inefficient row-by-row updates.

Example:

C#
foreach (var user in users)
{
    user.IsActive = false;
}

await context.SaveChangesAsync();

This requires:

  • entity tracking
  • multiple updates
  • additional memory overhead

i) Modern EF Core Approach

C#
await context.Users
    .Where(x => x.LastLogin < cutoffDate)
    .ExecuteUpdateAsync(x =>
        x.SetProperty(u => u.IsActive, false));

ii) Why This Matters

Bulk operations:

  • reduce memory usage
  • minimize roundtrips
  • avoid materialization
  • improve throughput significantly

This is one of the most impactful improvements introduced in newer EF Core versions.

12. DbContext Lifetime Mistakes

Improper DbContext lifetime management can create serious production issues.

i) Common Mistakes

a) Singleton DbContext

DbContext is not thread-safe.

Using it as a singleton can lead to:

  • race conditions
  • memory leaks
  • stale tracking data
  • unpredictable behavior

b) Long-Lived Contexts

Keeping contexts alive too long increases:

  • memory consumption
  • tracked entities
  • change tracker complexity

ii) Recommended Lifetime

In ASP.NET Core, DbContext should usually be registered as scoped:

C#
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

This aligns naturally with the request lifecycle.

13. The Fastest Query Is the One You Never Execute

At some point, query optimization alone is no longer enough.

Read-heavy systems eventually require caching strategies.

Examples include:

  • Redis
  • in-memory caching
  • distributed caching
  • response caching

i) Important Perspective

Caching should not compensate for fundamentally poor query design.

A bad query cached is still a bad query.

However, well-designed caching dramatically improves:

  • latency
  • throughput
  • scalability
  • infrastructure cost

ii) Think in Query Boundaries

Senior backend systems deliberately define:

  • what must be strongly consistent
  • what can be eventually consistent
  • what can be cached safely

Performance engineering is ultimately about balancing:

  • correctness
  • complexity
  • scalability
  • operational cost

14. Conclusion: Think Beyond LINQ Syntax

EF Core performance is rarely about clever LINQ tricks.

It is about understanding what your code becomes after translation.

High-performance backend systems are built by engineers who think carefully about:

  • SQL generation
  • query shape
  • indexing
  • data movement
  • network cost
  • materialization
  • concurrency

ORMs are productivity tools.

But abstractions do not remove the need to understand the underlying systems.

The engineers who build scalable APIs successfully are usually the ones who:

  • inspect generated SQL
  • understand database execution behavior
  • minimize unnecessary data retrieval
  • optimize bottlenecks pragmatically
  • introduce complexity only when justified

EF Core is extremely capable.

The real challenge is learning how to use it intentionally under production constraints.

About the Author

Ian Macharia

Ian Macharia

Admin

Ian is a Senior Software Engineer and Tech Lead specializing in building high-performance APIs, distributed systems, and modern cloud architectures.

Chat on WhatsApp