Connection Pooling Problems: Timeouts, Leaks and Max Pool Size

The application times out getting a connection, while SQL Server appears to have plenty of room. Connection pooling can exhaust an application's available connections before the database refuses a new login.

An almost empty canoe rack on a dock with one red canoe left, while unreturned canoes sit on the far shore

Identify Which Timeout You Received

A pool-acquisition timeout means the application waited for an available connection in its pool. The .NET client reports it as "Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool." A login timeout concerns establishing a connection. A command timeout concerns executing work on an acquired connection. Their messages and ownership differ, so begin with the exact error and client logs.

I ask whether the failure happened before or after a connection was acquired. That question keeps the investigation from turning into random server changes. The database cannot execute a query that the application never got a connection to submit.

This article uses SQL Server's .NET client pooling behavior as the concrete example. Other client implementations have their own defaults and pool rules. The DMV queries are read-only server observations. They help explain the symptoms, but cannot expose every client pool's borrowed-versus-idle state.

Understand the Scope of the Connection Pooling Limit

The default Max Pool Size in the .NET client is 100 connections per pool. It is not a limit of 100 sessions across SQL Server. Each application process can maintain its own pools, and one process can have several distinct pools.

When all eligible connections in a pool are occupied, another acquisition waits until one becomes available or its wait budget expires. Increasing the maximum can postpone the symptom while increasing database concurrency. That can worsen pressure if slow queries are keeping connections borrowed too long.

Compare the pool's limit with expected concurrent requests and their connection-holding time. One connection per briefly executing request is different from one connection held through a long external call. A connection pool is a lending desk, not an unlimited warehouse of patience.

Connection Strings Can Multiply Pools

Pools are separated by connection-string identity, with exact-string differences affecting this model. Reordering equivalent keywords can create a separate pool. Different database names, application names, authentication identities, or connection options also divide what looks like one application into multiple connection populations.

For integrated authentication, the Windows identity affects separation as well. Transaction enlistment and related client options can influence which connection is eligible for reuse. Standardize string construction in one approved application path rather than assembling variants for each request.

Inspect differences in Initial Catalog, Application Name, Integrated Security, Min Pool Size, Max Pool Size, and MultipleActiveResultSets. Do not paste secrets into diagnostic messages. Compare redacted settings and the approved construction code. The server sees resulting sessions, not the exact grouping key maintained inside the application process.

Group Sessions by Application and Host

The next query groups user sessions by program, host, and login. Its sleeping count describes session status, not whether the application returned the connection to a pool. A client can hold a borrowed connection without currently executing a request.

Use approved server monitoring permissions. These labels are supplied by the client and are not reliable security identity evidence. They are useful diagnostic dimensions. Also record application process counts, because several processes using the same labels still maintain distinct client pools.

Which process and pool are actually timing out? A server-wide total cannot answer that alone. Compare these groups with client pool telemetry, request concurrency, and the time each request holds its connection. Keep the application-side observations beside the DMV output.

SELECT program_name, host_name, login_name,
    COUNT_BIG(*) AS UserSessions,
    SUM(CONVERT(bigint, CASE WHEN status = N'sleeping' THEN 1 ELSE 0 END)) AS SleepingSessions,
    SUM(CONVERT(bigint, CASE WHEN open_transaction_count > 0 THEN 1 ELSE 0 END)) AS SessionsWithTransactions,
    MIN(login_time) AS EarliestLogin,
    MAX(last_request_end_time) AS LatestRequestEnd
FROM sys.dm_exec_sessions
WHERE is_user_process = 1 AND session_id <> @@SPID
GROUP BY program_name, host_name, login_name
ORDER BY UserSessions DESC;
Where the waiting caller comes from: a diagram about the connection pooling

Old Sleeping Sessions Are a Lead

An old last_request_end_time identifies a session whose last request ended well before the current observation. Sleeping pooled sessions can be normal. They remain open for reuse instead of reconnecting for every command. Age alone does not establish a leak.

The following query selects sleeping sessions idle beyond a chosen thirty-minute threshold. That threshold is a diagnostic input, not a definition of unhealthy behavior. Sessions with open transactions deserve closer inspection because their locks and transaction state can outlive the active request.

The timestamps describe server session activity. They do not reveal when the client checked a connection back into its pool. Combine the evidence with application instrumentation. Automatically terminating old sleeping sessions can disrupt legitimate reuse and conceal the code path that failed to release ownership.

SELECT session_id, program_name, host_name, login_name,
    login_time, last_request_start_time, last_request_end_time,
    open_transaction_count,
    DATEDIFF(minute, last_request_end_time, SYSDATETIME()) AS MinutesSinceRequestEnd
FROM sys.dm_exec_sessions
WHERE is_user_process = 1 AND session_id <> @@SPID
  AND status = N'sleeping'
  AND last_request_end_time < DATEADD(minute, -30, SYSDATETIME())
ORDER BY open_transaction_count DESC, last_request_end_time;

Tell Connection Pooling Leaks From Long Borrowing

A leak occurs when application ownership fails to release a connection as required. A slow query, blocked command, open reader, or long transaction can also keep a correctly owned connection occupied. Their symptoms overlap, but the fixes differ.

The next query joins active requests to their sessions. Blocking and long execution indicate why some connections remain busy. Sleeping sessions do not appear here, so compare it with the previous query rather than replacing that evidence. The request duration describes the current request, not the full client checkout interval.

SELECT s.session_id, s.program_name, s.host_name,
    r.status AS RequestStatus, r.command,
    r.blocking_session_id, r.wait_type, r.wait_time,
    r.cpu_time, r.total_elapsed_time,
    r.logical_reads, r.open_transaction_count
FROM sys.dm_exec_sessions AS s
JOIN sys.dm_exec_requests AS r ON r.session_id = s.session_id
WHERE s.is_user_process = 1 AND s.session_id <> @@SPID
ORDER BY r.total_elapsed_time DESC;

For connection pooling, the server connection creation time supplies another useful clue. A long-lived physical connection can be perfectly healthy if the application reuses it. Frequent new connections can instead suggest fragmentation, process churn, or disabled pooling. The following view helps correlate that behavior without pretending to report client checkout status.

SELECT s.session_id, s.program_name, s.host_name,
    c.connect_time, c.net_transport, c.auth_scheme,
    c.client_net_address, c.last_read, c.last_write
FROM sys.dm_exec_sessions AS s
JOIN sys.dm_exec_connections AS c ON c.session_id = s.session_id
WHERE s.is_user_process = 1 AND s.session_id <> @@SPID
ORDER BY c.connect_time;

Return Ownership on Every Code Path

Open the connection close to the database work and release it promptly afterward. Use deterministic disposal on success, exceptions, and cancellation paths. Close or dispose readers appropriately too. Garbage collection is not a reliable request-level release policy.

I review error and early-return paths when session populations grow under load. The happy path can be perfectly tidy while one exception path keeps every connection borrowed. Instrument acquisition and release around the actual owning scope. Include how long the application holds the connection outside SQL execution.

Keep external network calls and user interaction outside database transactions where the design permits. A sleeping session with an unfinished transaction can still block needed work. Transaction completion and connection release are separate responsibilities, and both need correct failure handling.

Adjust Connection Pooling Capacity After Explaining Demand

Before raising Max Pool Size, fix leaks and reduce unnecessary holding time. Then load-test the intended limit against database capacity and the application's concurrency model. Increasing every process's maximum also multiplies the potential total connections reaching the server.

Clearing pools or restarting the application can relieve the immediate symptom, but it removes useful evidence. Capture the affected process, settings, checkout timings, and server observations first when possible. A temporary reset should accompany a tracked diagnosis, not become the weekly operating plan.

Connection pooling works when ownership is short, release is reliable, and pool identity is consistent. Use SQL Server sessions to understand the database side. Use application evidence to prove which pool is exhausted and why its connections remain unavailable.

Related reading on this blog: SET XACT_ABORT ON: Stopping Timeouts From Leaving Open Transactions and Find Total Sessions by Database.

Leak or long borrowing?: a checklist on the connection pooling

A pool timeout is not proof that SQL Server ran out of sessions, it is a request for a connection the client could not supply.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.

SQL Connection, SQL DMV, SQL Error Messages, SQL Server
Previous Post
Table Partitioning Is Not Sharding: What Partitioning Really Gives You
Next Post
SQL SERVER – Adding Column Defaulting to Current Datetime in Table

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.