How to execute queries in parallel using EF Core

When multiple threads attempt to access a single DbContext instance simultaneously, the system typically fails, manifesting as an InvalidOperationException. This error frequently includes a specific warning: "A second operation started in this context before the previous operation was completed." This failure occurs because the DbContext is designed as a stateful unit of work, and its internal components—most notably the change tracker—cannot reconcile simultaneous modifications from different execution paths. To maintain data integrity and prevent corruption, Microsoft has built-in detection mechanisms that halt execution when overlapping operations are detected on a non-thread-safe context.
The Architecture of Concurrency in Data Access
The necessity for parallel query execution has grown alongside the rise of data-intensive web applications and microservices. In a standard dashboard scenario, an application might need to retrieve orders, system metrics, logs, and performance traces from a database. If these queries are executed sequentially, the total latency is the sum of all individual database round trips. By employing asynchronous parallelism via Task.WhenAll, developers can theoretically reduce this wait time to the duration of the single slowest query. However, because a standard DbContext instance relies on a single underlying database connection, it cannot support this parallel pattern out of the box.
Most relational databases, including Microsoft SQL Server, PostgreSQL, and Oracle, utilize a request-response communication model at the connection level. A single database connection is generally restricted to processing one command at a time. When a .NET application attempts to send a second query through the same connection before the first has returned its results, the protocol is violated. EF Core’s lack of thread safety is, therefore, not just a limitation of the software library but a reflection of the physical limitations of the underlying database protocols.
Technical Evolution and the Role of the Change Tracker
The decision to keep DbContext non-thread-safe is rooted in the fundamental trade-offs of software engineering. If Microsoft were to implement internal locking mechanisms within the DbContext to make it thread-safe, every database operation would incur a performance penalty due to the overhead of lock acquisition and release. For the vast majority of use cases, where a DbContext is scoped to a single web request and used sequentially, this overhead would be unnecessary and detrimental.
Central to this discussion is the EF Core Change Tracker. This component monitors every entity loaded into memory, recording its original state and any subsequent modifications. When a developer calls SaveChanges(), the Change Tracker provides the necessary delta to generate the appropriate SQL commands (INSERT, UPDATE, DELETE). Because the Change Tracker maintains an internal state representation of the database, allowing multiple threads to modify that state concurrently would lead to race conditions. For example, two threads might attempt to update the same property of a tracked entity simultaneously, leading to unpredictable data being persisted to the database.
Chronology of .NET Data Access Patterns
The evolution of thread management in EF Core can be traced through the history of the .NET framework itself. In the era of Entity Framework 6 (EF6), asynchronous programming was less pervasive, and the "one context per request" pattern was the gold standard. With the release of .NET Core and the subsequent rebranding to .NET 5, 6, and 7, the industry shifted heavily toward non-blocking I/O and high-concurrency models.
In 2020, with the release of EF Core 5.0, Microsoft introduced the IDbContextFactory interface. This was a pivotal moment in the framework’s timeline, as it provided an official, standardized way to handle scenarios where a single-threaded scoped context was insufficient. Before this, developers often resorted to manual instantiation or complex "service locator" patterns to generate fresh contexts. The introduction of the factory pattern allowed for the clean integration of DbContext into background services, Blazor Server applications, and parallel processing loops.
Implementing IDbContextFactory for Parallelism
To resolve the InvalidOperationException while maintaining the benefits of parallel execution, the industry standard has shifted toward the use of IDbContextFactory. This factory acts as a generator for short-lived DbContext instances. Instead of sharing a single context across multiple tasks, the application requests a fresh, isolated context for each individual task.
When a developer registers a DbContextFactory in the service collection, it is typically registered as a singleton or a scoped service. The factory’s CreateDbContext() method is designed to be extremely lightweight. By providing each parallel task with its own context, each task also receives its own dedicated database connection and change tracker. This isolation ensures that no two threads are competing for the same internal state or connection resources.
For instance, in a high-throughput product service, a read operation and an update operation can be dispatched simultaneously. By using the factory to generate a context for the "GetProduct" task and a separate context for the "UpdateStock" task, the application effectively utilizes two database connections in parallel. This approach bypasses the thread-safety limitations of the DbContext class by ensuring that the "unit of work" is strictly confined to the scope of the individual thread.
Performance Optimization via Context Pooling
While creating a new DbContext instance is relatively inexpensive, the initialization of the underlying options and internal services can add up in high-scale environments. To address this, EF Core provides "DbContext Pooling." By using AddPooledDbContextFactory, the framework maintains a cache of context instances that can be reused.
When an operation finishes with a context, it is reset and returned to the pool rather than being destroyed. When the factory is called again, it provides a pre-initialized instance from the pool. This optimization is particularly effective in cloud-native applications where minimizing CPU cycles and memory allocations is critical for reducing operational costs. Data suggests that context pooling can significantly improve throughput in scenarios involving hundreds of concurrent database operations per second.
Official Guidance and Industry Best Practices
Microsoft’s official documentation and senior architects emphasize that while parallelizing queries can improve latency, it must be balanced against the resources of the database server. Every parallel task requires a separate connection; therefore, running 50 queries in parallel will require 50 active connections in the connection pool. If the pool is exhausted, subsequent requests will hang, leading to a different set of performance bottlenecks.
Industry experts recommend a "measured parallelism" approach. Developers should identify the specific bottlenecks where sequential execution is causing a poor user experience and apply IDbContextFactory only in those targeted areas. Furthermore, for read-only operations, developers are encouraged to use the .AsNoTracking() method. This reduces the overhead of the Change Tracker entirely, further improving the performance of parallel read tasks.
Broader Implications for Software Architecture
The requirement for manual thread-safety management in EF Core has broader implications for how .NET applications are structured. It encourages a move away from "Fat DbContexts" that live too long and toward a more functional, stateless approach to data access. In modern architectures like Clean Architecture or Onion Architecture, the persistence layer must be carefully designed to ensure that the lifetime of the DbContext is strictly controlled.
Moreover, the rise of Blazor Server has brought these thread-safety issues to the forefront. Unlike traditional ASP.NET Core MVC applications, where a request is a short-lived event, Blazor Server maintains a persistent connection (via SignalR). If a developer injects a scoped DbContext into a Blazor component, that context remains alive as long as the user stays on the page. If the user triggers multiple UI updates simultaneously, the application will crash due to concurrent access. Consequently, the use of IDbContextFactory has become a mandatory best practice for Blazor development.
Conclusion and Future Outlook
As the .NET ecosystem continues to evolve toward Visual Studio 2026 and .NET 9 and 10, the focus on high-performance data access remains a priority. While the DbContext remains non-thread-safe by design, the tools surrounding it—such as the factory and pooling patterns—have matured significantly. Developers are no longer forced to choose between the safety of sequential execution and the speed of parallelism.
The strategic use of IDbContextFactory allows for the creation of robust, scalable applications that can handle the complexities of modern data requirements. By understanding the underlying mechanics of the Change Tracker and the constraints of database connection protocols, engineers can write code that is both highly performant and resilient to the pitfalls of concurrent data access. The future of EF Core development lies in this nuanced understanding of resource management, where thread safety is not a missing feature, but a deliberate architectural boundary that promotes better coding standards.







