DbContext Lifetime, Scoping and Connection Pooling

The DbContext lifetime determines how long a database connection is held and how state is shared between operations. Getting this wrong causes subtle bugs: a Singleton DbContext shared between concurrent requests produces race conditions; a Transient DbContext loses changes between operations; a background service DbContext clashes with the Scoped HTTP request lifecycle. ASP.NET Core’s default Scoped registration (one DbContext per HTTP request) is correct for web APIs. The exceptions are background services and parallel operations, which need special handling.

DbContext Lifetime Configuration

// โ”€โ”€ Standard Web API registration โ€” Scoped (one per HTTP request) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
builder.Services.AddDbContext<AppDbContext>(opts =>
    opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// Scoped = created at request start, disposed at request end
// All services that resolve AppDbContext in the same request share the same instance

// โ”€โ”€ AddDbContextPool โ€” for high-throughput APIs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Reuses DbContext instances from a pool instead of creating/destroying per request
// Significant performance improvement at high request rates
builder.Services.AddDbContextPool<AppDbContext>(opts =>
    opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")),
    poolSize: 128);   // keep up to 128 DbContext instances ready for reuse

// NOTE: DbContextPool resets the context (clears change tracker) between uses
// BUT: custom state added in OnModelCreating is NOT reset โ€” pools work well for
// standard usage but should not be used if you store custom request-specific
// state on the DbContext instance itself.

// โ”€โ”€ IDbContextFactory โ€” for background services and parallel operations โ”€โ”€โ”€โ”€
builder.Services.AddDbContextFactory<AppDbContext>(opts =>
    opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// Background service using IDbContextFactory (creates a new scope per operation)
public class PostIndexingService(
    IDbContextFactory<AppDbContext> dbFactory,
    ILogger<PostIndexingService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            // Create a fresh DbContext for each batch โ€” no shared state
            await using var db = await dbFactory.CreateDbContextAsync(ct);

            var unindexedPosts = await db.Posts
                .Where(p => p.IsPublished && !p.IsIndexed)
                .Take(100)
                .ToListAsync(ct);

            foreach (var post in unindexedPosts)
            {
                await IndexPostAsync(post, ct);
                post.MarkAsIndexed();   // tracked entity
            }

            await db.SaveChangesAsync(ct);
            await Task.Delay(TimeSpan.FromMinutes(1), ct);
        }
    }
}
Note: AddDbContextPool improves throughput by reusing DbContext instances rather than allocating new ones per request. At 1,000 requests per second, creating and destroying 1,000 DbContext objects has meaningful overhead. The pool maintains a set of pre-allocated contexts and checks them out/in per request. The change tracker is cleared between uses. The practical benefit is most significant in high-throughput microservices โ€” for typical CRUD APIs handling a few hundred requests per second, the difference is small. Test before optimising.
Tip: Use IServiceScopeFactory to create a scope for background services that need DbContext. This is the alternative to IDbContextFactory โ€” it creates an entire DI scope (not just a DbContext) which is useful when you also need Scoped services (like repositories): using var scope = scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();. Both patterns are correct; IDbContextFactory is simpler for DbContext-only needs, while IServiceScopeFactory is needed when the background service calls Scoped service methods that internally use DbContext.
Warning: Never inject a Scoped DbContext directly into a Singleton service. ASP.NET Core validates this at startup (or on first resolution) and throws an InvalidOperationException: "Cannot consume scoped service 'AppDbContext' from singleton 'MyService'". The fix: inject IDbContextFactory<AppDbContext> (registered as Singleton) or IServiceScopeFactory into the Singleton, and create a short-lived context per operation.

Multiple DbContexts โ€” Bounded Contexts

// โ”€โ”€ Separate DbContexts for different bounded contexts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Avoids a single "god" DbContext with every table
public class BlogDbContext : DbContext
{
    public DbSet<Post>    Posts    => Set<Post>();
    public DbSet<Comment> Comments => Set<Comment>();
    public DbSet<Tag>     Tags     => Set<Tag>();
}

public class IdentityDbContext : DbContext
{
    public DbSet<ApplicationUser> Users  => Set<ApplicationUser>();
    public DbSet<UserProfile>     Profiles => Set<UserProfile>();
}

// Each has separate migrations โ€” schema evolves independently
// builder.Services.AddDbContext<BlogDbContext>(opts => ...);
// builder.Services.AddDbContext<IdentityDbContext>(opts => ...);

Common Mistakes

Mistake 1 โ€” Singleton DbContext in a multi-threaded application (concurrent access crash)

โŒ Wrong โ€” DbContext registered as Singleton; concurrent HTTP requests share one context; random exceptions.

โœ… Correct โ€” DbContext must be Scoped; use IDbContextFactory for Singleton services that need DB access.

Mistake 2 โ€” Using Transient DbContext (a new instance per injection point in the same request)

โŒ Wrong โ€” Transient registration; Service A and Service B in the same request get different DbContext instances; SaveChangesAsync in A does not see Service B’s tracked changes.

โœ… Correct โ€” always use Scoped; all services in the same request share one DbContext.

🧠 Test Yourself

A background service (IHostedService) directly injects AppDbContext in its constructor. At runtime it throws InvalidOperationException. Why, and what is the fix?