EF Core Error: “A second operation was started on this context” – Complete Guide

EF Core Error: “A second operation was started on this context” – Complete Guide

📘 .NET / Entity Framework Core

A second operation was started on this context before a previous operation completed

Why it happens, how to fix it, and how to avoid it forever. Complete guide with code examples for EF Core 6/7/8.

🔍 What is this error?

System.InvalidOperationException

“A second operation was started on this context before a previous operation completed.”

This exception is thrown by Entity Framework Core when you try to start a new database operation (e.g., SaveChangesAsync(), ToListAsync(), FirstOrDefaultAsync()) on a DbContext instance while another asynchronous operation is still in progress on the same context instance.

📌 Key takeaway: DbContext is not thread‑safe and does not support concurrent operations. Each instance can only execute one asynchronous operation at a time.

⚡ Why does it happen?

The underlying cause is almost always one of these:

  • Missing await – You called an async method but forgot to await it, causing the next operation to start before the previous one finishes.
  • Parallel execution on the same DbContext – Using Task.WhenAll or Parallel.ForEach with the same context instance.
  • Reusing a context in a loop – Starting a new async operation inside a loop without waiting for the previous one.
  • Concurrent requests in a web application – If you register DbContext as a singleton (instead of scoped), multiple requests share the same instance and cause concurrency.

In all cases, the solution is to ensure that only one operation uses the DbContext at any given time.

🧩 Common scenarios (with code)

1️⃣ Missing await

You call an async method but ignore the returned task:

// ❌ BAD – missing await
var task = context.Products.ToListAsync();
var count = context.Products.Count(); // ⚡ Error! Previous operation still running.
// ✅ GOOD – await the async call
var products = await context.Products.ToListAsync();
var count = context.Products.Count(); // Now safe – previous operation completed.

2️⃣ Parallel operations with Task.WhenAll

// ❌ BAD – using the same context in parallel
var task1 = context.Orders.ToListAsync();
var task2 = context.Customers.ToListAsync();
await Task.WhenAll(task1, task2); // ⚡ Error! Both start on same context.
// ✅ GOOD – use separate contexts or sequential await
// Option A: Sequential
var orders = await context.Orders.ToListAsync();
var customers = await context.Customers.ToListAsync();

// Option B: Separate contexts (if truly parallel)
using var ctx1 = new AppDbContext();
using var ctx2 = new AppDbContext();
var task1 = ctx1.Orders.ToListAsync();
var task2 = ctx2.Customers.ToListAsync();
await Task.WhenAll(task1, task2);

3️⃣ Loop with async operations

// ❌ BAD – starting new operation before previous finishes
foreach (var id in ids)
{
    var entity = await context.Entities.FindAsync(id); // 🟢 first await is fine
    // but if you start another async operation inside the same loop iteration,
    // or start the next iteration without awaiting the previous one – you'll get the error.
}

Usually the error appears when you accidentally call SaveChangesAsync inside the loop without awaiting properly, or when you use Task.Run.

The fix: Always await every async call, and consider batching operations to reduce round-trips.

✅ How to fix it – complete solutions

🔹 1. Always await your async methods

Use the await keyword consistently. If you must fire-and-forget (rare), use a separate scope or background service.

🔹 2. Use a new DbContext instance for parallel operations

If you need to run multiple queries concurrently, create a separate context for each parallel branch.

using var ctx1 = new AppDbContext();
using var ctx2 = new AppDbContext();
var ordersTask = ctx1.Orders.ToListAsync();
var customersTask = ctx2.Customers.ToListAsync();
await Task.WhenAll(ordersTask, customersTask);

🔹 3. Register DbContext as scoped in DI (not singleton)

In ASP.NET Core, the default registration AddDbContext registers DbContext as scoped (per request). If you override it to singleton, multiple requests will share the same instance and cause this error.

// ✅ CORRECT (default)
builder.Services.AddDbContext<AppDbContext>(options => ...);

// ❌ AVOID this unless you have a very specific reason
builder.Services.AddSingleton<AppDbContext>(...);

🔹 4. Use AsNoTracking() for read‑only queries

This reduces overhead, but it doesn't directly solve concurrency – it just makes queries faster and reduces chance of change‑tracking conflicts.

🔹 5. Consider synchronization if you must share a context

You can use a SemaphoreSlim to serialize access, but this is generally an anti‑pattern. Better to use separate contexts.

private static readonly SemaphoreSlim _semaphore = new(1, 1);

public async Task<T> ExecuteSafelyAsync(Func<AppDbContext, Task<T>> operation)
{
    await _semaphore.WaitAsync();
    try
    {
        return await operation(_context);
    }
    finally
    {
        _semaphore.Release();
    }
}

But prefer the simpler approaches above.

📌 Best practices to avoid this error

  • Scope your DbContext appropriately – in web apps, use the default scoped lifetime (per request).
  • Always await async calls – never ignore the returned Task.
  • Use separate context instances for parallel work – don't share a context across threads.
  • Avoid long‑running operations – if you have a long-running query, consider splitting it or using AsSplitQuery() to reduce complexity.
  • Leverage dependency injection – let the DI container manage your DbContext lifetime.
  • Use using blocks or await using to ensure contexts are disposed properly.
💡 Pro tip: In a typical web API, the request–response cycle naturally isolates DbContext instances per request – which is why registering as scoped is the golden rule.

📖 Summary

The error “A second operation was started on this context before a previous operation completed” is a sign that your code is attempting to execute two asynchronous database operations on the same DbContext instance before the first one finishes.

Fix it by:

  • Always using await on async methods.
  • Not sharing a DbContext across parallel tasks – use separate instances.
  • Registering DbContext as scoped in DI.
  • Designing your data access layer to be synchronous‑only per context instance.

Following these patterns will keep your EF Core code robust, performant, and free of this common exception.


❓ Have a specific scenario? Share it in the comments or reach out – we're happy to help.

📘 Entity Framework Core Deep Dives – practical guides for .NET developers.

✍️ Written with ❤️ for the .NET community.

Post a Comment

0 Comments