Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,28 @@ The following example converts an existing connection string from using SQL Serv
</para>
</remarks>
</LoadBalanceTimeout>
<IdleTimeout>
<summary>
Gets or sets the maximum time, in seconds, that a connection can sit unused (idle) in the connection pool before it is discarded.
</summary>
<value>
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.IdleTimeout" /> property, or 300 (5 minutes) if none has been supplied.
Copy link
Copy Markdown
Contributor

@paulmedynski paulmedynski May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value is obviously the value of this property, so no need to state that. Maybe just:

"The idle timeout for pooled connections, in seconds."

And then mention the default value in the summary instead.

</value>
<remarks>
<para>
This property corresponds to the "Connection Idle Timeout" key within the connection string.
</para>
<para>
The driver makes a best effort to close connections that have remained idle in the pool for longer than this value. The exact point in the connection lifecycle at which the check occurs is an implementation detail and may change over time. This protects callers from receiving connections that may have been silently closed by firewalls, load balancers, or server-side inactivity thresholds.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close -> discard

</para>
<para>
A value of zero (0) disables idle expiration; connections are kept in the pool indefinitely (subject to other expiry rules such as <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.LoadBalanceTimeout" />).
</para>
<para>
Idle timeout operates independently of <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.LoadBalanceTimeout" />. Whichever threshold is exceeded first causes the connection to be recycled.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recycled -> discarded

</para>
</remarks>
</IdleTimeout>
<MaxPoolSize>
<summary>
Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,10 @@ public SqlConnectionStringBuilder(string connectionString) { }
[System.ComponentModel.DisplayNameAttribute("Load Balance Timeout")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public int LoadBalanceTimeout { get { throw null; } set { } }
/// <include file='../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/IdleTimeout/*'/>
[System.ComponentModel.DisplayNameAttribute("Connection Idle Timeout")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
public int IdleTimeout { get { throw null; } set { } }
/// <include file='../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml' path='docs/members[@name="SqlConnectionStringBuilder"]/MaxPoolSize/*'/>
[System.ComponentModel.DisplayNameAttribute("Max Pool Size")]
[System.ComponentModel.RefreshPropertiesAttribute(System.ComponentModel.RefreshProperties.All)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ internal static class DbConnectionStringDefaults
internal const bool IntegratedSecurity = false;
internal const SqlConnectionIPAddressPreference IpAddressPreference = SqlConnectionIPAddressPreference.IPv4First;
internal const int LoadBalanceTimeout = 0; // default of 0 means don't use
// Default configured idle timeout is 5 minutes. Connection pool behavior is gated by
// LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior for compatibility.
internal const int IdleTimeout = 300;
internal const int MaxPoolSize = 100;
Comment thread
priyankatiwari08 marked this conversation as resolved.
internal const int MinPoolSize = 0;
internal const bool MultipleActiveResultSets = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal static class DbConnectionStringKeywords
internal const string IntegratedSecurity = "Integrated Security";
internal const string IpAddressPreference = "IP Address Preference";
internal const string LoadBalanceTimeout = "Load Balance Timeout";
internal const string IdleTimeout = "Connection Idle Timeout";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this only apply to pooled connections? What happens if I specify this for a non-pooled connection?

If this only applies to pools, then maybe we should have Pool in the name somewhere? "Pooled Connection Idle Timeout"

internal const string MaxPoolSize = "Max Pool Size";
internal const string MinPoolSize = "Min Pool Size";
internal const string MultipleActiveResultSets = "Multiple Active Result Sets";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all
ShouldHidePassword = hidePassword;
State = state;
CreateTime = DateTime.UtcNow;
// Initialize the idle-since stamp to creation time so that a freshly built connection is treated
// as "just used" by idle-expiry checks until the pool's return path stamps it again on first return.
// Without this initialization, IdleSinceUtc would default to DateTime.MinValue, which would cause
// IsLiveConnection to immediately evict every new connection whenever IdleTimeout is configured.
IdleSinceUtc = CreateTime;
}

#region Properties
Expand All @@ -91,6 +96,14 @@ internal DbConnectionInternal(ConnectionState state, bool hidePassword, bool all
/// </summary>
internal DateTime CreateTime { get; }

/// <summary>
/// UTC timestamp of when this connection was last placed into the pool's idle state.
/// Stamped by <see cref="MarkPooledIdle"/> from the pool's return-to-pool path.
/// Internal setter exists to support deterministic unit tests without reflection.
/// Used by the pool to discard connections that have sat unused longer than the configured idle timeout.
/// </summary>
internal DateTime IdleSinceUtc { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should have "Utc" in this property name. If you change how CreateTime is initialized later, will "Utc" still be guaranteed - not likely.

The docs make it clear that the timestamp is in UTC, so I would suggest omitting the "Utc" suffix. If you decide to use local timestamps later, then you don't have to rename the property.


/// <summary>
/// The pool generation at the time this connection was created or added to the pool.
/// Used by <see cref="ChannelDbConnectionPool"/> to detect stale connections after a pool clear.
Expand Down Expand Up @@ -734,6 +747,16 @@ internal virtual void PrepareForReplaceConnection()
// By default, there is no preparation required
}

/// <summary>
/// Stamps <see cref="IdleSinceUtc"/> with the current UTC time. Called by the pool's return-to-pool path
/// (after the connection has been deactivated and is about to enter the idle pool) so that the pool can later
/// decide whether the connection has sat idle for too long and should be discarded.
/// </summary>
internal void MarkPooledIdle()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, we're confusing our concerns here. Is the pool responsible for managing the idleness of connections, or should the connection be responsible for its idleness based on knowing when it was returned into the pool?

If it's the former, then why not have the pool set IdleSinceUtc directly?

If it's the former, then this should be called ReturnedToPool().

I think some separation of policy and mechanism is warranted here:

  • The connection should be responsible for remembering when it was returned to the pool.
    • Rename this to ReturnedToPool(), and rename the property to ReturnedTime.
    • This method enforces UTC.
  • The pool should be responsible for using ReturnedTime to decide if a connection has been idle too long and discarding it.

{
IdleSinceUtc = DateTime.UtcNow;
}

internal void PrePush(DbConnection expectedOwner)
{
// Called by IDbConnectionPool when we're about to be put into it's pool, we take this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ public void ReturnInternalConnection(DbConnectionInternal connection, DbConnecti
}
else
{
// Stamp the idle-since timestamp immediately before putting the connection back in the
// pool so that IsLiveConnection can later evict it if it sits idle past the configured limit.
// Skip the stamp when idle expiry is disabled or legacy idle-timeout behavior is in effect
// to avoid the per-return DateTime.UtcNow on the hot return path.
if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior &&
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the optimization here, but it is imposing a correctness and maintenance burden as well. Have we measured the difference between using these conditions, and always calling MarkPooledIdle() ?

If we do keep these conditions, let's wrap them in a helper since they must always be checked together.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new connection pool has never had "legacy idle timeout behaviour", so should we even be checking the switch in this class? You can see this by the lack of an else block in IsLiveConnection() below - there is no legacy behaviour to preserve!

PoolGroupOptions.IdleTimeout != TimeSpan.Zero)
{
connection.MarkPooledIdle();
}
var written = _idleChannel.TryWrite(connection);
Debug.Assert(written, "Failed to write returning connection to the idle channel.");
}
Expand Down Expand Up @@ -424,6 +433,19 @@ public bool TryGetConnection(
/// <returns>Returns true if the connection is live and unexpired, otherwise returns false.</returns>
private bool IsLiveConnection(DbConnectionInternal connection)
{
// Connection has been sitting idle longer than the configured idle timeout.
// Checked before the (potentially expensive) liveness probe so an idle-expired
// connection is discarded without an SNI round-trip.
// IdleSinceUtc is initialized to CreateTime so a freshly minted connection never trips this
// check on first retrieval, and is then stamped by ReturnInternalConnection on every return.
TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout;
Comment thread
mdaigle marked this conversation as resolved.
if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior &&
idleTimeout != TimeSpan.Zero &&
DateTime.UtcNow > connection.IdleSinceUtc + idleTimeout)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparison is susceptible to overflow (throws). Re-arrange it like this to avoid:

DateTime.UtcNow - connection.IdleSinceUtc > idleTimeout

You may also want to explicitly check for underflow (i.e. if IdleSinceUtc is in the future).

{
return false;
}

// Broken physical connection
if (!connection.IsConnectionAlive())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal sealed class DbConnectionPoolGroupOptions
private readonly int _maxPoolSize;
private readonly int _creationTimeout;
private readonly TimeSpan _loadBalanceTimeout;
private readonly TimeSpan _idleTimeout;
private readonly bool _hasTransactionAffinity;
private readonly bool _useLoadBalancing;

Expand All @@ -22,7 +23,8 @@ public DbConnectionPoolGroupOptions(
int maxPoolSize,
int creationTimeout,
int loadBalanceTimeout,
bool hasTransactionAffinity
bool hasTransactionAffinity,
int idleTimeout
)
{
_poolByIdentity = poolByIdentity;
Expand All @@ -36,6 +38,16 @@ bool hasTransactionAffinity
_useLoadBalancing = true;
}

if (idleTimeout < 0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use unit then? Or TimeSpan?

{
throw new ArgumentOutOfRangeException(nameof(idleTimeout), idleTimeout, "Idle timeout cannot be negative.");
}

if (0 != idleTimeout)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swap backwards operands:

if (idleTime != 0)

Also, do we have an upper bound? Does max int really make sense?

{
_idleTimeout = TimeSpan.FromSeconds(idleTimeout);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this throw? If yes, did we document it?

}

_hasTransactionAffinity = hasTransactionAffinity;
}

Expand All @@ -54,6 +66,14 @@ public TimeSpan LoadBalanceTimeout
{
get { return _loadBalanceTimeout; }
}
/// <summary>
/// The maximum time a pooled connection can sit unused (idle) in the pool before it is discarded
/// on the next retrieval attempt. <see cref="TimeSpan.Zero"/> disables idle expiration.
/// </summary>
public TimeSpan IdleTimeout
{
get { return _idleTimeout; }
}
public int MaxPoolSize
{
get { return _maxPoolSize; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,20 @@ internal WaitHandleDbConnectionPool(

lock (s_random)
{
// Random.Next is not thread-safe
_cleanupWait = s_random.Next(12, 24) * 10 * 1000; // 2-4 minutes in 10 sec intervals, WebData 103603
TimeSpan idleTimeout = connectionPoolGroup.PoolGroupOptions.IdleTimeout;
if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior && idleTimeout != TimeSpan.Zero)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is wrong - we will incorrectly apply the legacy behaviour if the switch is false and the idle timeout is zero.

We have 3 states:

  • Legacy (switch is true)
  • New (switch is false and timeout isn't zero)
  • None (switch is false and timeout is zero)

The last 2 are new behaviour we are introducing here.

{
// The WaitHandle pool takes two pruning cycles to remove an idle connection
// (new->old generation, then old->closed), so halve the configured timeout
// to approximate the requested idle lifetime.
long cleanupWaitMilliseconds = (long)idleTimeout.TotalMilliseconds / 2;
_cleanupWait = cleanupWaitMilliseconds >= int.MaxValue ? int.MaxValue : (int)cleanupWaitMilliseconds;
}
else
{
// Preserve the historical 2-4 minute random cleanup window.
_cleanupWait = s_random.Next(12, 24) * 10 * 1000; // 2-4 minutes in 10 sec intervals, WebData 103603
}
}

_connectionFactory = connectionFactory;
Expand Down Expand Up @@ -670,6 +682,10 @@ private void DeactivateObject(DbConnectionInternal obj)
// DelegatedTransactionEnded event will clean up the
// connection appropriately regardless of the pool state.
Debug.Assert(_transactedConnectionPool != null, "Transacted connection pool was not expected to be null.");
// Transacting connections are held in their own store and are never
// proactively closed (doing so would abort the transaction, which can be
// distributed). Idle-timeout enforcement does not apply here, so we do
// not stamp IdleSinceUtc when parking the connection in the transacted pool.
_transactedConnectionPool.PutTransactedObject(transaction, obj);
rootTxn = true;
}
Expand Down Expand Up @@ -1028,7 +1044,7 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj
Interlocked.Decrement(ref _waitCount);
obj = GetFromGeneralPool();

if ((obj != null) && (!obj.IsConnectionAlive()))
if ((obj != null) && (IsIdleExpired(obj) || !obj.IsConnectionAlive()))
{
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetConnection|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
Expand Down Expand Up @@ -1209,6 +1225,8 @@ private DbConnectionInternal GetFromTransactedPool(out Transaction transaction)
}
else if (!obj.IsConnectionAlive())
{
// Transacting connections are exempt from idle-timeout eviction (closing them
// would abort the transaction, possibly distributed). Only liveness is checked here.
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetFromTransactedPool|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
obj = null;
Expand Down Expand Up @@ -1329,13 +1347,35 @@ private void PutNewObject(DbConnectionInternal obj)

SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.PutNewObject|RES|CPOOL> {0}, Connection {1}, Pushing to general pool.", Id, obj.ObjectID);

// Stamp the idle-since timestamp immediately before placing the connection on the idle stack
// so that idle-expiry checks on later retrieval can decide whether it has sat unused too long.
// Skip the stamp when idle expiry is disabled (the default) to avoid the per-return
// DateTime.UtcNow on the hot return path.
if (!LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior &&
PoolGroupOptions.IdleTimeout != TimeSpan.Zero)
{
obj.MarkPooledIdle();
}
_stackNew.Push(obj);
_waitHandles.PoolSemaphore.Release(1);

SqlClientDiagnostics.Metrics.EnterFreeConnection();

}

/// <summary>
/// Returns true when the supplied connection has been sitting idle in the pool longer than the
/// configured <see cref="DbConnectionPoolGroupOptions.IdleTimeout"/>. Returns false when idle timeout
/// is disabled (zero).
/// </summary>
private bool IsIdleExpired(DbConnectionInternal obj)
{
TimeSpan idleTimeout = PoolGroupOptions.IdleTimeout;
Comment thread
mdaigle marked this conversation as resolved.
return !LocalAppContextSwitches.UseLegacyIdleTimeoutBehavior &&
idleTimeout != TimeSpan.Zero &&
DateTime.UtcNow > obj.IdleSinceUtc + idleTimeout;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about overflow here.

}

public void ReturnInternalConnection(DbConnectionInternal obj, DbConnection owningObject)
{
Debug.Assert(obj != null, "null obj?");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ internal static class LocalAppContextSwitches
private const string UseConnectionPoolV2String =
"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2";

/// <summary>
/// The name of the app context switch that controls whether to preserve
/// legacy idle-timeout behavior in connection pooling.
/// </summary>
private const string UseLegacyIdleTimeoutBehaviorString =
"Switch.Microsoft.Data.SqlClient.UseLegacyIdleTimeoutBehavior";

#if NET && _WINDOWS
/// <summary>
/// The name of the app context switch that controls whether to use the
Expand Down Expand Up @@ -222,6 +229,11 @@ private enum SwitchValue : byte
/// </summary>
private static SwitchValue s_useConnectionPoolV2 = SwitchValue.None;

/// <summary>
/// The cached value of the UseLegacyIdleTimeoutBehavior switch.
/// </summary>
private static SwitchValue s_useLegacyIdleTimeoutBehavior = SwitchValue.None;

#if NET && _WINDOWS
/// <summary>
/// The cached value of the UseManagedNetworking switch.
Expand Down Expand Up @@ -539,6 +551,16 @@ public static bool UseCompatibilityAsyncBehaviour
defaultValue: false,
ref s_useConnectionPoolV2);

/// <summary>
/// When set to true (the default), pooling preserves historical idle-timeout behavior.
/// When set to false, configured Connection Idle Timeout is enforced by the pool.
/// </summary>
public static bool UseLegacyIdleTimeoutBehavior =>
AcquireAndReturn(
UseLegacyIdleTimeoutBehaviorString,
defaultValue: true,
ref s_useLegacyIdleTimeoutBehavior);

#if NET && _WINDOWS
/// <summary>
/// When set to true, .NET on Windows will use the managed SNI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,8 @@ private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(Sql
opt.MaxPoolSize,
connectionTimeout,
opt.LoadBalanceTimeout,
opt.Enlist);
opt.Enlist,
opt.IdleTimeout);
}
return poolingOptions;
}
Expand Down
Loading
Loading