Entity Framework  

EF Core Code That Works Locally but Fails in Production (part 3)

Introduction

Some EF Core problems don’t appear as errors or warnings. Your application compiles, runs, and even passes testing. Yet once real users start using the system, APIs slow down, requests time out, and database load increases sharply. These issues usually surface only when data grows and traffic increases. The code itself looks correct, which makes the problem harder to identify. In most cases, the root cause is not EF Core itself, but how data is accessed at scale.

In this final part of the series, we’ll examine common EF Core patterns that quietly degrade performance in production environments and how to fix them correctly.

Scaling and Concurrency Mistakes

1. The N+1 Query Problem – When One Query Becomes Hundreds

One of the most common and dangerous EF Core performance issues is the N+1 query problem. It often goes unnoticed during development because small datasets hide the real cost.

The code:

var orders = await _context.Orders.ToListAsync();

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

At first glance, this looks fine. The issue is that Orders and Customers are stored in separate tables. EF Core loads only the orders initially. Each time order.Customer is accessed, EF Core sends a new query to the database.

What actually happens:

  • One query loads all orders

  • One query is executed per order to load its customer

With 100 orders, this results in 101 database queries. With thousands of orders, performance degrades rapidly.

The correct approach is to load related data eagerly.

The fix:

var orders = await _context.Orders
    .Include(o => o.Customer)
    .ToListAsync();

This generates a single SQL query with a join, reducing database round trips dramatically.

2. Forgetting Indexes – EF Core Is Not the Villain

Sometimes a query works perfectly in development but becomes extremely slow in production. This often leads developers to blame EF Core, even though the real problem lies in the database.

The code:

_context.Users.Where(u => u.Email == email);

If the Email column is not indexed, the database must scan the entire table for every request. As the table grows, response times increase significantly.

EF Core generates correct SQL here. The database simply isn’t optimized to handle the query efficiently.

The solution is to ensure proper indexing on frequently searched columns, especially those used in filters and joins.

3. Async in Name Only – Blocking the Thread Pool

Using async and await in controllers does not automatically make your application scalable. A common mistake is calling synchronous EF Core methods inside asynchronous code.

The code:

var users = _context.Users.ToList();

Even when this runs inside an async controller, it blocks the thread while waiting for the database. Under load, blocked threads pile up, reducing throughput and causing slow responses.

The correct approach is to use asynchronous EF Core methods consistently.

The fix:

var users = await _context.Users.ToListAsync();

This allows the thread to be released while waiting for the database, improving scalability and performance under concurrent load.

Conclusion

In this article, we have seen that most performance issues stem from executing queries too early and loading unnecessary data into memory. By ensuring that operations like counting, sorting, and filtering happen at the database level rather than in your application code, you prevent small inefficiencies from turning into production failures. Mastering these simple patterns ensures your EF Core code remains fast and reliable, even as your data grows. Hope this helps !