Skip to main content

Command Palette

Search for a command to run...

EF Core "A Second Operation Was Started on This Context" in ASP.NET Core: Root Cause and Fix

Published
โ€ข11 min read
EF Core "A Second Operation Was Started on This Context" in ASP.NET Core: Root Cause and Fix

Every ASP.NET Core team hits this at some point in production: a spike in traffic, a background job that fires under load, or a refactor that introduces parallel awaits โ€” and suddenly the logs light up with System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. It usually shows up intermittently, which makes it one of the more frustrating EF Core errors to chase down. The fix is straightforward once you understand the real cause; the difficulty is that the surface error rarely points at the actual problem. This same pattern also underlies several of the pitfalls covered in the EF Core Performance Tuning Checklist for High-Traffic APIs.

For those who want the full working implementation โ€” a complete ASP.NET Core API with proper DbContext lifetime management, IDbContextFactory patterns for background services, and scoped-vs-singleton safeguards โ€” the production-ready source code is on Patreon, wired up and ready to run.

Understanding this properly starts with the DbContext lifetime model, which is exactly what Chapter 3 of the ASP.NET Core Web API: Zero to Production course covers โ€” including why scoped lifetime exists, how the DI container manages context lifetimes per request, and what goes wrong when those boundaries break down.

ASP.NET Core Web API: Zero to Production

What "A Second Operation Was Started" Actually Means

The error message from EF Core is precise, even if it sounds cryptic at first:

System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

EF Core's DbContext is not thread-safe. This is a deliberate design decision, not an oversight. A single context instance tracks entity state, manages the change tracker, and holds a live database connection โ€” none of which are safe to share across threads. EF Core detects when concurrent access occurs and throws this exception to protect you from undefined behavior, data corruption, and phantom writes that would otherwise be silent.

The key phrase in the error is "usually caused by different threads" โ€” but the threading in question is often indirect. You don't need two explicit threads; a Task.WhenAll, a fire-and-forget Task.Run, or an unawaited async call can produce exactly the same problem.

The Four Root Causes in Production

1. Singleton or Static Injection of a Scoped DbContext

The most common root cause in real-world ASP.NET Core APIs is injecting a DbContext (which is registered as scoped) into a singleton service. The scoped context gets captured at singleton creation time, then reused across every request โ€” violating the per-request lifetime the DI container was designed to provide.

// โŒ This captures a scoped DbContext in a singleton โ€” a ticking clock in production
public class ProductCacheService
{
    private readonly AppDbContext _db; // captured once, shared forever

    public ProductCacheService(AppDbContext db)
    {
        _db = db;
    }
}

ASP.NET Core's DI container will actually throw a InvalidOperationException at startup in Development mode when it detects scoped-inside-singleton registration โ€” but only if scope validation is enabled. In Production, it silently allows it unless you explicitly configure the container to validate scopes.

2. Concurrent Awaits on the Same Context Instance

A subtler cause is calling multiple async EF Core methods without ensuring they complete sequentially. Two awaits on the same context instance, fired at the same logical moment, are enough to trigger the exception.

// โŒ Both queries run against the same DbContext instance simultaneously
var productsTask = _db.Products.Where(p => p.IsActive).ToListAsync();
var categoriesTask = _db.Categories.ToListAsync();
await Task.WhenAll(productsTask, categoriesTask);

Even though both calls look like clean async code, Task.WhenAll starts both operations before either completes โ€” and EF Core sees two concurrent operations on one context.

3. Accessing a Disposed Context in Background Services

Background services (BackgroundService, IHostedService) run as singletons. If they inject a scoped DbContext directly via constructor injection, the context's DI scope expires after the first request cycle โ€” and every subsequent database call runs against a disposed context.

This produces a mix of ObjectDisposedException and the second-operation error, depending on timing and whether the context was garbage-collected yet.

4. Fire-and-Forget Tasks That Outlive the Request Scope

A _ = Task.Run(async () => { await _db.SaveChangesAsync(); }) pattern in a request handler fires a background task that continues after the request completes and the scope is disposed. By the time SaveChangesAsync runs, the context is already disposed. This is almost always the cause when the error appears in logs but developers cannot reproduce it locally under low load.

How to Diagnose It

Enable Scope Validation in Production

Add scope validation to your DI container configuration. This catches scoped-inside-singleton issues at startup rather than at runtime under load:

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});

ValidateOnBuild causes the DI container to scan all registered services at application startup and throw immediately if any scoped service is captured inside a singleton. This is one of the cheapest and most effective production safeguards available.

Read the Stack Trace Carefully

The stack trace for this exception will point to the EF Core internals, but the frames just above them will show which service initiated the second operation. Look for:

  • A Task.WhenAll or Task.Run near the top of the application frames

  • A BackgroundService.ExecuteAsync call path

  • A method that calls two EF Core methods without awaiting the first

Add Structured Logging Around Concurrent DB Calls

In a high-traffic production environment where the error is intermittent, add structured log entries around every place where multiple EF Core calls occur in the same method. Correlate log entries by CorrelationId or RequestId to identify which request triggered the collision.

The Fixes

Fix 1: Never Inject a Scoped DbContext Into a Singleton

If you need database access in a singleton service, inject IServiceScopeFactory instead and create a new scope for each unit of work:

// โœ… Create a fresh scope and context for each operation in a singleton
public class ProductCacheService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ProductCacheService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task RefreshCacheAsync()
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        // db is scoped to this operation only
    }
}

This is the canonical solution. The scope โ€” and the DbContext inside it โ€” is created fresh for each operation and disposed when the using block exits. No shared state, no threading violation.

Fix 2: Use IDbContextFactory for Fine-Grained Lifetime Control

For background services and high-parallelism scenarios, IDbContextFactory<TContext> gives you explicit control over when a context is created and disposed, without relying on the DI scope lifetime:

// Register in Program.cs
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(connectionString));
// โœ… Create and dispose a context per operation
public class DataSyncService : BackgroundService
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public DataSyncService(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await using var db = await _contextFactory.CreateDbContextAsync(stoppingToken);
            // db is fully owned by this iteration
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

IDbContextFactory is the right tool for background services, Blazor Server components, and any scenario where the standard request-scoped DI lifetime does not align with your unit of work.

Fix 3: Never Await Two EF Core Calls in Parallel on the Same Context

Replace Task.WhenAll patterns with sequential awaits when both queries share a context:

// โœ… Sequential awaits โ€” safe on a single context instance
var products = await _db.Products.Where(p => p.IsActive).ToListAsync();
var categories = await _db.Categories.ToListAsync();

If you genuinely need parallelism for performance, create two separate context instances โ€” one per query โ€” rather than sharing one.

Fix 4: Eliminate Fire-and-Forget Database Calls

Any Task.Run or _ = pattern that touches EF Core after the request scope has been disposed must be replaced with a proper background job. Use IHostedService, System.Threading.Channels, or a durable job library like Hangfire for work that needs to outlive the request.

How to Prevent It Recurring

Enable scope validation on build โ€” catches the silent DI misconfiguration before it reaches production.

Audit every singleton that takes a service dependency โ€” if any of those dependencies are scoped (including DbContext, HttpContext, or request-scoped services), inject IServiceScopeFactory instead.

Treat all Task.WhenAll with EF Core as a code smell โ€” unless you are explicitly managing separate context instances per parallel operation, sequential awaits are the safe default.

Prefer IDbContextFactory for all background and scheduled work โ€” it makes context lifetime explicit and eliminates the entire class of scope-mismatch errors that show up in BackgroundService implementations. If you are still deciding between IHostedService and BackgroundService for long-running background work, the Background Services in .NET 10 Enterprise Decision Guide covers the trade-offs in detail.

Add ValidateScopes = true and ValidateOnBuild = true to both Development and Production โ€” the startup cost is negligible, and the protection is permanent.

โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!

FAQ

What causes "A second operation was started on this context" in EF Core?

The error is thrown when two database operations are attempted concurrently on the same DbContext instance. The most common causes are: a scoped DbContext injected into a singleton service, concurrent async operations using Task.WhenAll on the same context, and background services that create a context without proper scope management.

Is DbContext thread-safe in EF Core?

No. DbContext is explicitly not thread-safe by design. Microsoft's documentation states that DbContext instances should not be shared across threads. Each thread or unit of work should have its own context instance, either via DI scoping or IDbContextFactory.

How do I use EF Core in a BackgroundService or IHostedService?

Register IDbContextFactory<TContext> via AddDbContextFactory in your DI setup, then inject the factory into your background service. For each unit of work, call CreateDbContextAsync() to get a fresh context, use it within an await using block, and let it be disposed when the block exits. This gives you explicit, safe lifetime control independent of the request pipeline.

What is IDbContextFactory and when should I use it?

IDbContextFactory<TContext> is a factory that creates DbContext instances on demand. It is the recommended approach for scenarios where the standard request-scoped DI lifetime does not apply โ€” background services, Blazor Server components, parallel data processing, and multi-tenancy scenarios where you need a context per tenant. It was introduced in EF Core 5.0 and is the canonical fix for DbContext threading issues outside the request pipeline.

How do I detect scoped-inside-singleton DI misconfiguration?

Add ValidateScopes = true and ValidateOnBuild = true to UseDefaultServiceProvider in your Program.cs. With ValidateOnBuild, the DI container scans all registrations at startup and throws immediately if a scoped service is captured inside a singleton โ€” before any request ever runs. Without this, the misconfiguration can go undetected for months until traffic patterns expose it.

Does Task.WhenAll always cause this error with EF Core?

Not always โ€” but it is a high-risk pattern. Task.WhenAll starts all tasks before any complete. If two tasks both initiate EF Core queries against the same context instance, they will run concurrently and trigger the error. The safe alternatives are: sequential await calls, or using a separate context instance per parallel operation.

Can I make DbContext thread-safe by using lock?

Technically you can serialize access with a lock, but this approach is strongly discouraged. It introduces contention, reduces throughput, and still leaves the change tracker in an inconsistent state if one operation fails mid-flight. The correct fix is to never share a context instance across concurrent operations โ€” not to lock around the sharing.

Why does this error only appear under load and not locally?

The error requires concurrent operations to hit the same context instance at the same time. Under low local traffic, requests arrive sequentially and the race condition never materializes. Under production load, multiple requests arrive concurrently โ€” and if a singleton is holding a shared context, or a background job fires at the same time as a request, the collision becomes frequent enough to appear in logs. This is why the error is often described as "intermittent" โ€” it is load-dependent.


For the complete production implementation โ€” including IDbContextFactory usage in background services, scope validation setup, and a fully wired example API โ€” see Patreon for the source code.

Want to deepen your EF Core and ASP.NET Core API fundamentals? Chapter 3 of the Zero to Production course covers DbContext lifetime, threading pitfalls, and production-safe DI patterns in full detail.

More ASP.NET Core deep-dives at codingdroplets.com | YouTube

More from this blog

C

Coding Droplets

198 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.