This post explains how connection pooling works with EF Core, the benefits of implementing a Singleton connection pool, and how to set it up correctly.
Using the Singleton Connection Pool with Entity Framework Core
Getting Started
Efficient database connectivity is a critical part of building scalable .NET applications. In high-traffic scenarios—such as web APIs or background services—creating and disposing of database connections too frequently can lead to significant performance bottlenecks.
This is where connection pooling and the singleton connection pool pattern come into play. In this article, we’ll explore how connection pooling works in Entity Framework Core (EF Core), why you might consider using a singleton connection pool, and how to implement it correctly.
Understanding Connection Pooling in EF Core
When you use EF Core with a relational provider like SQL Server, PostgreSQL, or MySQL, the underlying ADO.NET provider manages connection pooling automatically.
A connection pool keeps a cache of open database connections that can be reused, instead of opening a new connection for every request. This drastically reduces overhead from repeated connection establishment.
For example, when EF Core executes a query:using var context = new AppDbContext();
var data = context.Users.ToList();
The context requests a connection from the pool. Once disposed, the connection is returned to the pool for reuse.
The Singleton DbContext Is Not the Same Thing
A common misconception is that a singleton DbContext improves performance. In reality, DbContext should never be registered as a singleton because it is not thread-safe and is designed to be short-lived (per operation or per request).
Instead, the correct approach is to make sure the underlying connection pool (not the context) is shared efficiently across requests.
Why Consider a Singleton Connection Pool?
Under normal circumstances, EF Core and ADO.NET already provide connection pooling automatically, as long as your connection string is identical across instances.
However, there are cases where you might explicitly want to manage a singleton connection pool:- Custom connection management: You need to control connection lifetimes, max pool size, or timeouts.
- Database multiplexing or dynamic connection strings: You’re switching between multiple databases and need separate pools for each.
- High-frequency service calls: Your application opens and closes many connections per second.
DbContext factory or using a pooled DbContext pattern can improve performance.
Using EF Core’s Built-in Context Pooling
The simplest and safest way to enable efficient pooling in EF Core is to use AddDbContextPool instead of AddDbContext in your dependency injection setup.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add EF Core DbContext with pooling
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);
How It Works
- EF Core maintains an internal pool of
AddDbContextinstances. - Each time a context is requested, EF Core reuses a pre-warmed instance instead of creating a new one.
- When the context is released, EF Core resets its state and returns it to the pool.
This approach avoids the cost of building and disposing contexts repeatedly, while still maintaining thread safety and the unit-of-work pattern.
Implementing a Singleton Connection Pool Manually
If you need finer control—such as using a shared connection factory—you can manually create a singleton DbConnection pool. Here’s a simplified example using SQL Server:
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSingletonConnectionPool(this IServiceCollection services, string connectionString)
{
// Create a shared SqlConnectionFactory
services.AddSingleton(() => new SqlConnection(connectionString));
services.AddDbContext<AppDbContext>((provider, options) =>
{
var connection = provider.GetRequiredService<SqlConnection>();
options.UseSqlServer(connection);
});
return services;
}
}
Caution:
Sharing a single SqlConnection across multiple threads is not safe. If you do this, you must ensure access synchronization or use a proper connection factory that retrieves connections from ADO.NET’s internal pool.
Thread-Safe Singleton Connection Pool Example (C#)
Here’s a complete, thread-safe, and production-safe implementation that works with SQL Server or PostgreSQL.
using System;
using System.Data.Common;
using Microsoft.Data.SqlClient; // or Npgsql for PostgreSQL
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public sealed class DbConnectionPoolManager
{
private static readonly Lazy<DbConnectionPoolManager> _instance =
new(() => new DbConnectionPoolManager());
private string? _connectionString;
private Func<DbConnection>? _connectionFactory;
private readonly object _lock = new();
private DbConnectionPoolManager() { }
public static DbConnectionPoolManager Instance => _instance.Value;
public void Initialize(string connectionString)
{
if (_connectionFactory != null)
return; // already initialized safely
lock (_lock)
{
if (_connectionFactory == null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
// Create factory lazily; this ensures thread-safe init
_connectionFactory = () => new SqlConnection(_connectionString);
}
}
}
/// <summary>
/// Returns a new (pooled) connection. The ADO.NET provider will handle pooling.
/// </summary>
public DbConnection GetOpenConnection()
{
if (_connectionFactory == null)
throw new InvalidOperationException("ConnectionPoolManager not initialized. Call Initialize() first.");
var conn = _connectionFactory();
conn.Open(); // pulls from ADO.NET pool
return conn;
}
}
Why This Is Thread Safe
DbConnectionPoolManageris a true singleton, created via Lazy<T>.- Initialization uses a lock to prevent race conditions.
- Each thread/request calls GetOpenConnection() → receives a fresh connection from ADO.NET’s pool.
- The pool is managed by the driver, not manually by your code.
- No shared mutable connection objects are reused across threads.
A better pattern is to let the ADO.NET provider handle pooling automatically, while you manage only the configuration.
Configuring ADO.NET Connection Pool Settings
You can control connection pooling behavior directly in your connection string Instead of managing your own singleton, you can often just use:
Server=myServer;Database=myDb;User Id=myUser;Password=myPass;
Max Pool Size=100;Min Pool Size=5;Pooling=true;
| Setting | Description |
|---|---|
Pooling |
Enables or disables connection pooling (default: true) |
Max Pool Size |
Maximum number of connections in the pool |
Min Pool Size |
Minimum number of connections maintained |
Connection Lifetime |
Maximum lifetime (in seconds) for a connection in the pool |
- Use AddDbContextPool whenever possible — it’s safe, simple, and efficient.
- Avoid singleton DbContext instances — they cause concurrency issues.
- Use a consistent connection string so the same pool is reused.
- Monitor your pool — use SQL Server’s DMVs (sys.dm_exec_connections, sys.dm_os_performance_counters) or Application Insights.
- Benchmark your configuration under realistic load to tune pool sizes
Summary
While Entity Framework Core and ADO.NET already manage connection pooling for you, understanding and controlling the singleton connection pool pattern can help you optimize performance in demanding scenarios.
Thanks