If your ASP.NET app on Windows hosting throws intermittent SqlException timeouts under modest traffic, the SQL Server instance is rarely “down.” More often the app pool is holding connections open, the command timeout is fighting a table scan, or Entity Framework is opening more sessions than the pool can recycle. That pattern shows up the same way on SQL Server 2022 and on SQL Server 2025 GA.
Treat timeouts as a hosting-floor problem first: connection string, IIS app pool lifetime, query shape, then engine features. The fixes below are the ones that clear tickets fastest for .NET apps talking to a remote SQL instance over a normal hosting network path.
#Connection pooling is not optional
ADO.NET pools by exact connection-string match. Change a single keyword, user, or Application Name and you get a second pool. On a shared or reseller Windows box that multiplies sockets against the SQL host and burns through worker threads in w3wp.exe. Keep one canonical string per database identity and let the pool do its job.
<connectionStrings>
<add name="AppDb"
connectionString="Server=sql.example;Database=AppDb;User ID=app_user;Password=***;
Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;
Application Name=MyAspNetApp;TrustServerCertificate=True;"
providerName="Microsoft.Data.SqlClient" />
</connectionStrings>
For ASP.NET Core, the same keywords belong in appsettings.json (or environment variables) and flow into Microsoft.Data.SqlClient. Prefer Min Pool Size only when cold starts are painful; oversized minimums waste SQL worker threads on quiet sites. Max Pool Size of 100 is a sane default for one app pool; if you run multiple sites against one login, lower each site’s max so the sum stays under the SQL login’s connection limit.
- Never build connection strings with string concat per request — that defeats pooling.
- Dispose SqlConnection, SqlCommand, and EF DbContext every time (using / await using).
- Recycle the IIS app pool on a schedule if you still see pool fragmentation after a bad deploy.
#Separate connect timeout from command timeout
Connection Timeout (connect timeout) is how long SqlClient waits to open a socket and complete login. Command timeout is how long a single batch may run. Raising connect timeout to 60 seconds hides network or firewall problems and ties up ASP.NET request threads. Keep connect timeout short (10–15s). Raise command timeout only for known heavy reports, not for every OLTP call.
// .NET 10 / Microsoft.Data.SqlClient
await using var conn = new SqlConnection(cs);
await conn.OpenAsync(ct); // respects Connection Timeout in the string
await using var cmd = conn.CreateCommand();
cmd.CommandText = "dbo.GetOrderSummary";
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = 30; // seconds; 0 = wait forever — avoid in web apps
cmd.Parameters.AddWithValue("@OrderId", orderId);
await using var reader = await cmd.ExecuteReaderAsync(ct);
In EF Core 10, default command timeout is 30 seconds. Override per context or per call for imports, not globally to 0. Pair that with cancellation tokens from the ASP.NET request so abandoned browsers do not leave queries running on SQL Server.
#Indexes and plans beat blind timeout hikes
A 30-second timeout on a missing index is still a failure. For hosted web apps, capture the plan that actually ran: Query Store (on by default in recent SQL Server cumulative baselines for new DBs) or an actual plan from a staging copy of production data. Look for scans on large tables in the hot path — login, cart, order lookup — not for perfect indexes on every column.
-- Targeted covering index for a common web lookup
CREATE INDEX IX_Orders_CustomerId_OrderDate
ON sales.Orders (CustomerId, OrderDate DESC)
INCLUDE (Status, TotalAmount)
WITH (ONLINE = ON); -- ONLINE supported on editions that allow it
-- Quick check: who is waiting right now
SELECT r.session_id, r.status, r.wait_type, r.cpu_time, r.total_elapsed_time,
t.text
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id <> @@SPID
ORDER BY r.total_elapsed_time DESC;
SQL Server 2025 continues the Query Store and intelligent query processing line; use those when you control the instance. On a managed or shared SQL catalog you may only get indexes and statistics you create in your database — still enough to fix most ASP.NET list and detail pages. Update statistics after large imports; stale stats produce the same “works on my backup” plan flips people blame on hosting.
#EF Core habits that exhaust the pool
EF Core 10 is fine on SQL Server 2022/2025 if you avoid chatty patterns. N+1 queries, unbounded Include graphs, and long-lived DbContext instances pinned in a singleton are the usual offenders. Each open reader holds a pooled connection until it is disposed.
- Register DbContext with AddDbContext (scoped). Do not stash it in static fields or Singletons.
- Prefer projection (Select) over wide Includes for API responses.
- Use AsNoTracking() for read-only endpoints.
- Batch writes with ExecuteUpdate/ExecuteDelete where you do not need change tracking.
If you must run a long report, push it to a background worker or a separate app pool identity with its own constrained Max Pool Size so it cannot starve the public site. On IIS, set the app pool to always-on only when you understand the memory cost; idle timeout that recycles a leaked context is sometimes the only safety net on shared hosts.
#Backups are not a performance feature — but restores are a drill
Hosting control panels and maintenance plans handle full/diff/log backups; your job is knowing RPO and practicing restore to a side database before you need it. After restore, rebuild or reorganize fragmented indexes and update statistics before you point a staging slot at the copy. A “fast” app against empty tables teaches nothing about production plans.
Practical takeaway: lock one pooled connection string, keep connect timeout short and command timeout intentional, dispose every context and connection, and fix the top two missing indexes on your hottest queries before you raise any timeout again. On Windows Server with IIS, watch app pool recycles and SqlClient pool counters together — when both climb, the bug is almost always in app code or indexing, not in the SQL Server service itself.
Comments
No comments yet