SqlConnection late and close it per request/invocation; let ADO.NET pooling do the reuse. Size the pool (Min/Max Pool Size), enable Microsoft.Data.SqlClient configurable retry logic, and prefer Managed Identity. You'll get quicker connection opens, fewer transient failures, and simpler operations.
Open() calls into cheap checkouts.Max Pool Size so one instance cannot overwhelm SQL.| Area | Default / Common | Enhanced (recommended) | Impact |
|---|---|---|---|
| Pooling | Pooling=true, Min=0, Max=100 |
Min=10–20 to pre‑warm, keep Max≈100 (tune with telemetry) |
Faster cold starts, stable concurrency under load. |
| Retries | Classic ConnectRetry* only |
Enable configurable retry logic (exponential, bounded) | Hides transient network blips without masking real faults. |
| Auth | SQL Login in app settings | Managed Identity / AAD | No stored secrets; easier rotation & RBAC. |
| Timeouts | Command Timeout unspecified |
Set per scenario (e.g., 30–60s for APIs, 300s for batch) | Prevents hangs; encourages back‑pressure. |
| HA | Default connect routing | MultiSubnetFailover=true for AG listeners |
Faster failover / first‑open on multi‑subnet AGs. |
AddDbContextPool + compiled queries.Max Pool Size ≈ concurrent requests × DB ops/request.// Program.cs
builder.Services.AddDbContextPool<AppDb>(o => o.UseSqlServer(cs, sql => sql.EnableRetryOnFailure()));
Command Timeout (batch/ETL).// Startup once
SqlConnection.RetryLogicProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(
new SqlRetryLogicOption { NumberOfTries=4, DeltaTime=TimeSpan.FromSeconds(2), MaxTimeInterval=TimeSpan.FromSeconds(10) }
);
AddDbContextPool (context pooling) plus ADO.NET pooling.EnableRetryOnFailure) in addition to SqlClient retry.// Program.cs
builder.Services.AddDbContextPool<AppDb>(o =>
o.UseSqlServer(cs, sql => sql.EnableRetryOnFailure())
.EnableSensitiveDataLogging(false));
// Compiled query example
public static readonly Func<AppDb,int,Task<Car?>> GetCarById =
EF.CompileAsyncQuery((AppDb db, int id) =>
db.Cars.AsNoTracking().Where(c => c.Id == id).FirstOrDefault());
ExecuteAsync/QueryAsync with explicit commandTimeout.await using var conn = new SqlConnection(cs);
await conn.OpenAsync(ct);
var rows = await conn.QueryAsync<CarDto>(
"select Id, Make, Model from dbo.Cars where Status=@s",
new { s = "Active" }, commandTimeout: 60);
var compiler = new SqlServerCompiler();
var q = new Query("Cars").Where("Status","Active").Select("Id","Make","Model");
var sql = compiler.Compile(q);
var cars = await conn.QueryAsync<CarDto>(sql.Sql, sql.NamedBindings, commandTimeout: 60);
IN (@ids) query.// Resolver (pseudo)
public Task<IReadOnlyList<Car>> GetCarsByIdsAsync(IReadOnlyList<int> ids) =>
_dataLoader.LoadAsync(ids);
Server=tcp:<server>.database.windows.net,1433;
Database=<db>;
Authentication=Active Directory Managed Identity;
Encrypt=True;TrustServerCertificate=False;
Application Name=<app-name>;
MultipleActiveResultSets=True;
Pooling=True;Min Pool Size=10;Max Pool Size=100;
ConnectRetryCount=3;ConnectRetryInterval=5;
Command Timeout=300;
Server=tcp:<server>.database.windows.net,1433;
Database=<db>;
Authentication=Active Directory Default;
Encrypt=True;TrustServerCertificate=False;
Application Name=<app-name>;
Pooling=True;Min Pool Size=0;Max Pool Size=100;
Connect Timeout=30;Command Timeout=300;
Server=tcp:<listener>,1433;
Database=<db>;
Authentication=Active Directory Managed Identity;
Encrypt=True;TrustServerCertificate=False;
Application Name=<app-name>;
Application Intent=ReadOnly;MultiSubnetFailover=True;
Pooling=True;Min Pool Size=5;Max Pool Size=80;
Connect Timeout=15;Command Timeout=120;
Microsoft.Data.SqlClient over System.Data.SqlClient. New features and better .NET Core support land here first. (Waqas Kamal)Persist Security Info=false, Connection Timeout=30, Pooling=true, and Min/Max Pool Size sized to your traffic; watch for pool fragmentation if max is reached. (Waqas Kamal)Application Name for observability. (Aireforge)SqlDataReader, and use SqlBulkCopy for large loads. (Reintech)Min Pool Size).Command Timeout; review server-side waits/locks.Max Pool Size; batch at app layer (DataLoader, set-based ops).Connecting to a database isn’t free. A client has to spin up a driver, open a TCP socket, authenticate, run some work, then close both the connection and the socket. If you repeat those steps for every request at scale, the server wastes time and memory on setup/teardown instead of queries. A connection pool keeps a small fleet of ready-made connections and hands them out on demand, so most requests skip that expensive setup and finish faster.
After you set sane pooling and retry options, you should see faster requests and lower resource churn. Use your cloud metrics for end-to-end latency and watch SQL’s DMVs for active sessions, connection states, and which apps are connected. Those two views will confirm you’re getting the benefits you expected.
Open()A SqlConnection session negotiates TLS/TDS, authenticates, and allocates server resources. ADO.NET keeps a per-connection-string pool of idle sessions so most Open() calls are O(1) checkouts. Close/Dispose returns to the pool. Open late, close early.
| Area | Default-ish | Enhanced | Why it helps |
|---|---|---|---|
| Pooling | Min=0, Max=100 | Min=10–20, Max tuned to concurrency | Warm starts, stable throughput, fewer cold opens |
| Retries | Classic ConnectRetry* | Configurable SqlClient retry (exponential, bounded) | Absorbs transient network blips |
| Auth | SQL login | Managed Identity / AAD | No secrets, RBAC, simpler rotation |
| Timeouts | Implicit | Connect=30s; Command=60s (APIs) / 300s (batch) | Predictable latency and back-pressure |
| HA/Routing | Default | MultiSubnetFailover=True (AG), read-only hints where applicable | Faster failover & read scale |
using Microsoft.Data.SqlClient;
SqlConnection.RetryLogicProvider =
SqlConfigurableRetryFactory.CreateExponentialRetryProvider(
new SqlRetryLogicOption { NumberOfTries = 4, DeltaTime = TimeSpan.FromSeconds(2), MaxTimeInterval = TimeSpan.FromSeconds(10) });
Server=tcp:<server>.database.windows.net,1433;
Database=<db>;
Authentication=Active Directory Managed Identity;
Encrypt=True;TrustServerCertificate=False;
Application Name=<app>;
Pooling=True;Min Pool Size=10;Max Pool Size=100;
ConnectRetryCount=3;ConnectRetryInterval=5;
Command Timeout=300;
Server=tcp:<server>.database.windows.net,1433;
Database=<db>;
Authentication=Active Directory Default;
Encrypt=True;TrustServerCertificate=False;
Application Name=<app>;
Pooling=True;Min=0;Max=100;Connect Timeout=30;Command Timeout=300;
Server=tcp:<server>.database.windows.net,1433;Database=<db>;
User ID=<user>;Password=<secret>;Encrypt=True;TrustServerCertificate=False;
Application Name=<app>;Pooling=True;Min Pool Size=10;Max Pool Size=100;
ConnectRetryCount=3;ConnectRetryInterval=5;Command Timeout=300;
SqlConnection.Command Timeout higher for ETL/Timer jobs.AddDbContextPool for EF; set Application Name.Per-operation context; never share DbContext across concurrent circuit calls.
Do not connect directly to SQL; use an API. Optionally add offline with SQLite + sync.
Context pooling + compiled queries; AsNoTracking() for reads; EF EnableRetryOnFailure.
Parameterize; explicit commandTimeout; use SqlBulkCopy for big inserts.
Compose → compile to SQL+parameters → execute with Dapper.
Batch with DataLoader and run a single IN (@ids) query. Consider a read-only pool for list queries.
Set Application Name. Track Open() and command durations; correlate with DMVs.
SELECT program_name, COUNT(*) AS sessions FROM sys.dm_exec_sessions WHERE is_user_process=1 GROUP BY program_name;
SELECT session_id, command, wait_type, cpu_time, reads FROM sys.dm_exec_requests WHERE session_id <> @@SPID;
Canonicalize strings; differences create new pools.
concurrency ≈ RPS_instance × DB_ops_per_request × p95_DB_seconds
Retire elderly connections on return; start 300–600s if needed.
...; Application Intent=ReadWrite; ......; Application Intent=ReadOnly; MultiSubnetFailover=True; ...
Default off; enable only for overlapping readers on one connection.
Races multiple IPs; measure Open() p95.
8192 is fine for most; only change for giant LOB/bulk with measurement.
Pooling minimizes new sockets; NAT Gateway provides more ephemeral ports & stable egress.
using var bulk = new SqlBulkCopy(conn) { DestinationTableName = "dbo.Staging" };
var tvp = new DataTable(); /* add rows */
// INSERT ... VALUES (...),(...),(...) with parameters
APIs: scoped connection per request; Functions: per-invocation open/close; Blazor/Mobile: no direct SQL.
...; Authentication=Active Directory Managed Identity; Pooling=True; Min Pool Size=10; Max Pool Size=100; Connect Timeout=30; Command Timeout=60; Application Name=My.Api;
...; Pooling=True; Min Pool Size=10; Max Pool Size=80; ConnectRetryCount=3; ConnectRetryInterval=5; Command Timeout=60; Application Name=My.Dapper.Api;
...; Application Intent=ReadOnly; MultiSubnetFailover=True; Pooling=True; Min=5; Max=80; Connect Timeout=15; Command Timeout=60; Application Name=My.GraphQL;