Skip to main content
Entity Framework Data Access

7 EF Core Data Access Mistakes That Slow Down Your App and How to Fix Them

Why Your EF Core App Might Be Slower Than It Should BeEntity Framework Core (EF Core) is a beloved object-relational mapper (ORM) in the .NET ecosystem, but it's often blamed for performance issues. The truth is, many slowdowns stem from how developers interact with the ORM rather than from EF Core itself. Common mistakes like fetching too much data, ignoring indexing, or misusing navigation properties can cause your application to lag under load. In this guide, we'll dissect seven frequent data access errors and show you how to fix them. We'll use composite scenarios based on real projects to illustrate each problem, and provide clear, actionable solutions. By the end, you'll have a mental checklist to audit your own code and avoid these pitfalls.The Hidden Cost of Context LifetimeOne of the first mistakes many teams make is misunderstanding the DbContext lifetime. In a typical web application, you should use a

Why Your EF Core App Might Be Slower Than It Should Be

Entity Framework Core (EF Core) is a beloved object-relational mapper (ORM) in the .NET ecosystem, but it's often blamed for performance issues. The truth is, many slowdowns stem from how developers interact with the ORM rather than from EF Core itself. Common mistakes like fetching too much data, ignoring indexing, or misusing navigation properties can cause your application to lag under load. In this guide, we'll dissect seven frequent data access errors and show you how to fix them. We'll use composite scenarios based on real projects to illustrate each problem, and provide clear, actionable solutions. By the end, you'll have a mental checklist to audit your own code and avoid these pitfalls.

The Hidden Cost of Context Lifetime

One of the first mistakes many teams make is misunderstanding the DbContext lifetime. In a typical web application, you should use a short-lived context per request. Keeping a context alive for too long can lead to stale data and memory bloat. For example, a team once kept a single DbContext for the entire user session, resulting in steadily degrading response times. Switching to a per-request context resolved the issue immediately. Always register your DbContext as scoped in ASP.NET Core dependency injection to ensure each request gets a fresh instance.

Why Performance Matters from Day One

Performance tuning is often deferred until production issues arise, but by then, the codebase may have accumulated many small inefficiencies. Designing your data access layer with performance in mind from the start can save hours of refactoring. The mistakes we'll cover are common precisely because they're easy to overlook during development. Let's dive into each error and its fix.

Mistake 1: The N+1 Query Problem

The N+1 query is one of the most notorious performance killers in any ORM. It occurs when you first load a parent entity and then loop through its children, triggering a separate database query for each child. For example, loading a list of orders and then accessing each order's line items in a foreach loop will send one query for the orders and one query per order for line items. If you have 100 orders, that's 101 queries. This quickly becomes catastrophic under load.

How to Identify N+1

You can spot N+1 by enabling EF Core's logging to see the SQL generated. Look for repeated identical queries with different parameter values. Another telltale sign is response times that grow linearly with the number of parent entities. Many profiling tools, like MiniProfiler, can automatically detect N+1 patterns and alert you.

The Fix: Use Eager Loading with Include

The most straightforward fix is to use the .Include() method to load related data in a single query. For our orders example, you'd write context.Orders.Include(o => o.LineItems).ToList(). This generates a JOIN query that fetches all data in one round trip. For deeper relationships, chain multiple Includes or use .ThenInclude(). However, be careful not to over-include; loading all related entities can result in massive Cartesian products. Use selective includes based on your use case.

Alternative: Explicit Loading

If you only need related data for a subset of parent entities, explicit loading with .Entry().Collection().Load() can be more efficient than eager loading everything. This pattern is useful when you conditionally need child data based on business logic. The key is to batch your loads to avoid the 1+N trap—load all needed children in one query using a shared foreign key list.

Remember, N+1 is often invisible during development with small datasets but becomes painfully obvious at scale. Make it a habit to review your loops for implicit lazy loading and apply eager or explicit loading where appropriate.

Mistake 2: Ignoring Database Indexing

Even the most efficient queries will perform poorly if the underlying tables lack proper indexes. EF Core generates SQL based on your LINQ queries, but it cannot create indexes for you. A missing index on a foreign key column can turn a simple join into a full table scan. In one project, a query that filtered orders by customer ID took over 10 seconds because the CustomerId column had no index. Adding the index reduced it to 50 milliseconds.

Common Indexing Gaps

Developers often forget to index columns used in WHERE, ORDER BY, JOIN, or GroupBy clauses. Also, composite indexes can speed up queries that filter on multiple columns. For example, an index on (Status, CreatedDate) can accelerate a query that filters by status and orders by date. Use SQL Server's missing index DMVs or EF Core's logging to detect slow queries and identify missing indexes.

How to Add Indexes with EF Core

You can define indexes using data annotations or the Fluent API. For instance, [Index(nameof(CustomerId))] on the Order entity creates an index on that column. In migrations, this translates to CREATE INDEX statements. Review your migrations to ensure indexes are created early. Also, consider filtered indexes for queries that always include a constant condition, like WHERE IsDeleted = 0.

Trade-Offs and Monitoring

Indexes speed up reads but slow down writes. On high-throughput tables, too many indexes can degrade insert/update performance. Monitor index usage and remove unused indexes. Use database tuning advisors to find the right balance. A well-indexed database can often eliminate the need for complex caching layers, so invest time upfront to analyze your query patterns.

In summary, treat indexing as part of your EF Core design, not an afterthought. Every time you add a new query, check the execution plan and ensure the relevant columns are indexed.

Mistake 3: Overusing Lazy Loading

Lazy loading is convenient—navigation properties are automatically fetched when accessed. But it's also a major source of the N+1 problem. If you enable lazy loading globally without thinking, any iteration over parent entities that touches child properties will trigger additional queries. Moreover, lazy loading can cause unexpected database round trips in views or API serialization, leading to performance disasters.

When Lazy Loading Might Be Acceptable

Lazy loading can be acceptable in desktop or admin applications with low concurrency, where the developer has full control over when navigation properties are accessed. In web applications with many simultaneous users, it's almost always better to disable lazy loading and use explicit or eager loading. EF Core 2.1+ made lazy loading opt-in via the UseLazyLoadingProxies extension method. Consider not adding it at all.

How to Disable and Replace It

To disable lazy loading, simply don't configure it. If you already have it enabled, remove the call to UseLazyLoadingProxies. Then, refactor your code to use .Include() for queries that need related data. For scenarios where you don't know upfront what data is needed, use explicit loading with .Load(). This gives you fine-grained control and avoids accidental queries.

Serialization Pitfalls

Another risk with lazy loading is serialization. When an API controller returns an entity, JSON serializer may traverse all navigation properties, triggering hundreds of queries. Always project to DTOs or use [JsonIgnore] on navigation properties to prevent this. Better yet, use AutoMapper or manual projection with .Select() to return only the data you need.

In short, disable lazy loading by default in web applications. It's a safety net that often becomes a trap. Take the time to design your data access explicitly—your database will thank you.

Mistake 4: Fetching Too Much Data with Select N+1

Even without lazy loading, you can still fetch unnecessary data. A common anti-pattern is selecting entire entities when only a few columns are needed. This is sometimes called the "Select N+1" problem because it mirrors the N+1 pattern but with columns. For example, loading all columns of an Order entity just to display the order date and total in a list view wastes bandwidth and memory.

Project to Anonymous Types or DTOs

The fix is to use .Select() to project only the required fields. For instance, context.Orders.Select(o => new OrderDto { Id = o.Id, Date = o.OrderDate, Total = o.Total }). This instructs EF Core to generate a SELECT clause with only those columns, resulting in a leaner query. Additionally, projection automatically disables change tracking, reducing overhead.

AutoMapper and ProjectTo

If you use AutoMapper, the .ProjectTo() method can simplify projections by leveraging your mapping configuration. It translates to a .Select() expression under the hood, so it's just as efficient. However, be cautious with complex mappings that might still fetch extra data. Always review the generated SQL.

Paginated Queries and Count

When implementing pagination, avoid fetching all rows to count them. Use a separate query with .Count() that is efficient, and then fetch only the page of data. Combine projection with pagination for maximum efficiency: context.Orders.Where(...).Select(dto).Skip(10).Take(10).ToList(). This minimizes data transfer and speeds up response times.

Remember, every byte fetched from the database adds to latency. Project only what you need, and your app will feel snappier.

Mistake 5: Misusing Transactions and Concurrency

Transactions are essential for data integrity, but wrapping every operation in a transaction can lead to long-running locks and deadlocks. EF Core by default uses implicit transactions for single SaveChanges calls. However, developers sometimes explicitly create DbContext.Database.BeginTransaction() for a series of operations, keeping the transaction open longer than necessary.

When Explicit Transactions Are Needed

Explicit transactions are required when you need to atomically commit changes across multiple SaveChanges calls or across multiple DbContexts. But for a single SaveChanges, the implicit transaction is sufficient. If you do use explicit transactions, keep them short. Avoid holding a transaction while waiting for user input or performing long-running external API calls.

Concurrency Conflicts

Another common issue is optimistic concurrency without proper handling. EF Core uses a concurrency token (e.g., a Timestamp column or RowVersion) to detect conflicts. If you don't handle DbUpdateConcurrencyException, users may overwrite each other's changes silently. Implement retry logic or refresh the entity and reapply changes.

Pessimistic Locking in EF Core

For high-contention scenarios, you might need pessimistic locking using WITH (UPDLOCK) hints. EF Core allows raw SQL via FromSqlRaw or using ExecuteSqlRaw with table hints. However, use this sparingly as it reduces concurrency. A better approach is to design your application to minimize conflicts by partitioning data or using queue-based processing.

In summary, use transactions deliberately, keep them short, and always handle concurrency exceptions. Test your transaction logic under load to ensure it doesn't become a bottleneck.

Mistake 6: Ignoring Query Caching and Compiled Queries

EF Core caches the compiled form of LINQ queries, but the first execution still incurs compilation overhead. For very frequently called queries—like fetching a user by ID—you can pre-compile queries to skip this step. The EF.CompileQuery method (or EFCore.CompiledQuery in earlier versions) creates a delegate that caches the query plan.

How to Create a Compiled Query

Define a static compiled query that takes a context and parameters: private static readonly Func GetUserById = EF.CompileQuery((AppDbContext ctx, int id) => ctx.Users.FirstOrDefault(u => u.Id == id));. Then use it as var user = GetUserById(context, 42);. This eliminates query compilation on each call, which can yield a 10-20% performance improvement for hot paths.

Second-Level Caching

EF Core doesn't have built-in second-level caching, but you can implement it with libraries like EFCoreSecondLevelCacheInterceptor. This caches query results in memory or distributed cache (Redis). Use it for read-heavy, rarely updated data. However, be cautious with cache invalidation—stale data can cause subtle bugs. A TTL (time-to-live) approach is simpler but may serve outdated data.

When to Avoid Caching

Avoid caching queries that return dynamic data or are part of write-heavy workflows. Also, don't cache large result sets; that defeats the purpose. Profile your application to identify which queries are executed most often and would benefit from caching. A combination of compiled queries for frequently called simple queries and second-level cache for reference data is a solid strategy.

Remember, caching adds complexity. Start with compiled queries and only add second-level caching if profiling shows it's needed.

Mistake 7: Poor DbContext Design and Unit of Work

The DbContext is the unit of work in EF Core. A common mistake is using a single large context class with many DbSets, leading to tight coupling and performance issues. For example, a monolithic context with 50+ entities will load metadata for all entities even if you only need one. This increases startup time and memory usage.

Bounded Contexts to the Rescue

Apply the bounded context pattern from Domain-Driven Design: create smaller DbContexts that each handle a specific subdomain. For instance, an OrderingContext and a CatalogContext instead of one big context. This isolates changes and reduces the metadata footprint. Each context should be registered as scoped independently.

Managing Multiple Contexts

When using multiple contexts, be careful with transactions that span contexts. Distributed transactions are not always supported. Consider using eventual consistency or the Outbox pattern to coordinate between contexts. Also, avoid sharing entities between contexts; each context should own its aggregates.

Connection Pooling and Resilience

EF Core uses ADO.NET connection pooling by default, but misconfiguring the connection string can degrade performance. Ensure Max Pool Size is set appropriately (default 100). Use EnableRetryOnFailure for transient fault handling, especially in cloud environments. A well-designed context with proper connection management can handle hundreds of requests per second.

In summary, design your DbContexts to match your business boundaries. Keep them lean, and test their performance under realistic loads. This architectural decision pays dividends as your application grows.

Frequently Asked Questions

This section answers common questions about EF Core performance that we hear from developers. Use it as a quick reference when auditing your own code.

Should I disable lazy loading completely?

For most web applications, yes. Lazy loading is convenient but often leads to N+1 queries. If you must use it, be diligent about including related data eagerly or loading it explicitly. Consider it a last resort.

How can I monitor EF Core performance in production?

Use Application Insights, Serilog with SQL logging, or MiniProfiler. Enable EF Core's sensitive data logging (with caution) to capture SQL. Set up alerts for slow queries. Many tools also provide query duration histograms.

Is it okay to use raw SQL instead of LINQ?

Yes, for complex queries that LINQ cannot express efficiently. Use FromSqlRaw or ExecuteSqlRaw for performance-critical paths. But be aware that you lose compile-time checking and are responsible for SQL injection prevention. Use parameterized queries always.

What about async/await with EF Core?

Always use async methods (ToListAsync, SaveChangesAsync) in web applications to avoid blocking threads. This improves scalability. But don't overuse async in desktop apps where it adds overhead without benefit.

How do I handle large result sets?

Use streaming with AsEnumerable or AsAsyncEnumerable to avoid loading the entire set into memory. For reports, use server-side paging with Skip and Take. Consider using raw SQL with server-side cursors if needed.

Can I use EF Core with NoSQL databases?

EF Core is primarily for relational databases. For NoSQL, consider the Cosmos DB provider or dedicated SDKs. EF Core's relational features (joins, transactions) don't map well to NoSQL.

What's the biggest mistake you see?

The N+1 query problem remains the most common and damaging mistake. It's often introduced inadvertently when adding a new feature without reviewing the generated SQL. A simple code review can catch it.

Conclusion: Build Faster Data Access Today

We've covered seven critical mistakes that can slow down your EF Core application: N+1 queries, missing indexes, lazy loading overuse, fetching too much data, transaction misuse, ignoring caching, and poor DbContext design. Each mistake has a straightforward fix, but the key is to make these practices part of your development workflow.

Start by enabling SQL logging in your development environment. Review the generated queries for each new feature. Add indexes based on your query patterns. Project to DTOs early. Disable lazy loading by default. Use compiled queries for hot paths. And design your DbContexts around your business domains. These steps will not only speed up your application but also make your code more maintainable.

Performance is a continuous journey. Use profiling tools to identify bottlenecks, and don't assume EF Core is the culprit—often, it's how we use it. Apply the fixes discussed here, and you'll see immediate improvements. Happy coding!

About the Author

This article was prepared by the editorial team for this publication. We focus on practical explanations and update articles when major practices change.

Last reviewed: May 2026

Share this article:

Comments (0)

No comments yet. Be the first to comment!