EF Core Connection Management Behavior
EF Core does not guarantee the release of the physical connection after each command if the DbContext remains undisposed. By default, EF Core opens a connection upon the first execution of a command (such as SaveChanges or ToList) and holds that connection open for the remainder of the DbContext lifetime to optimize performance for subsequent operations.
The Impact of Async Boundaries and Provider Pooling
When a DbContext is held across asynchronous boundaries (e.g., long-running await points), the underlying physical connection remains checked out from the ADO.NET provider pool (such as Microsoft.Data.SqlClient). This prevents other requests from utilizing that connection, which can lead to pool exhaustion even if the database itself is not under heavy load.
The CloseConnection behavior (or calling Database.CloseConnection()) forces the physical connection to be returned to the provider pool immediately after a command completes. While this increases the frequency of connection check-outs/check-ins, it significantly reduces the risk of exhaustion in scenarios where DbContext instances have long lifetimes or are used in highly concurrent asynchronous workflows.
Distinguishing EF Core Management from ADO.NET Exhaustion
To determine if the issue is rooted in EF Core's lifetime management or general provider pool limits, monitor these specific signals:
- EF Core Logging: Enable the
Microsoft.EntityFrameworkCore.Database.Connection category. A pattern of ConnectionOpened events without corresponding ConnectionClosed events during a single request indicates the DbContext is holding the connection.
- Exception Type: ADO.NET pool exhaustion typically manifests as a
SqlException with a message stating "Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool". This is distinct from a command timeout, which occurs after a connection is already established.
- Database Metrics: Query
sys.dm_exec_connections or sys.dm_db_connection_stats to see if the number of active sessions matches the number of active DbContext instances.
Resolution Steps
- Reduce DbContext Scope: Ensure
DbContext is registered as Scoped (per-request) rather than Singleton or Transient in long-lived services.
- Explicitly Close Connections: If the
DbContext must persist across long async gaps, call context.Database.CloseConnection() manually after the primary database interaction.
- Verify Pool Size: Check the connection string
Max Pool Size (default is usually 100). If the application legitimately requires more concurrent connections than the pool allows, increase this value.
Diagnostic Requirement: To refine this recommendation, please provide the specific DbContext service lifetime (e.g., Scoped, Singleton) and the EF Core version currently in use.