Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
126f16a
Add configurable idle connection timeout (ADO #39970)
priyankatiwari08 May 19, 2026
2a94926
Address Copilot review feedback on #4295
priyankatiwari08 May 20, 2026
93ab7ee
Address PR #4295 inline review feedback
priyankatiwari08 May 21, 2026
dd30ce7
Address PR review: default IdleTimeout=300, remove synonym, drop tran…
priyankatiwari08 May 26, 2026
b3a7b12
Address PR #4295 review feedback on idle timeout behavior
priyankatiwari08 May 27, 2026
a6a69c5
Remove Pool Idle Timeout synonym; keep only canonical Connection Idle…
priyankatiwari08 May 27, 2026
a37358d
Address review feedback for idle connection timeout
priyankatiwari08 May 29, 2026
f32d6cd
Address Paul's review feedback: rename and doc cleanup
priyankatiwari08 Jun 4, 2026
0f0d018
Apply review feedback: drop guard, overflow-safe expiry, Yoda swap, 3…
priyankatiwari08 Jun 4, 2026
911b83a
Re-add legacy-switch guard around ReturnedToPool stamp
priyankatiwari08 Jun 4, 2026
228fe6f
Merge branch 'main' into feature/idle-connection-timeout-pr1
priyankatiwari08 Jun 4, 2026
80da656
Fix merge conflict damage in LocalAppContextSwitchesHelper
priyankatiwari08 Jun 4, 2026
7611570
Fix merge conflict damage in LocalAppContextSwitches
priyankatiwari08 Jun 4, 2026
abfa301
Pass idleTimeout to DbConnectionPoolGroupOptions in budget test
priyankatiwari08 Jun 5, 2026
5b1bbe9
Add TimeoutTimer arg to TryGetConnection / idleTimeout to DbConnectio…
priyankatiwari08 Jun 5, 2026
0cf4d4a
Address PR #4295 Copilot review: document UseLegacyIdleTimeoutBehavio…
priyankatiwari08 Jun 5, 2026
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 on its next retrieval.
Comment thread
mdaigle marked this conversation as resolved.
Outdated
</summary>
<value>
The value of the <see cref="P:Microsoft.Data.SqlClient.SqlConnectionStringBuilder.IdleTimeout" /> property, or 0 if none has been supplied.
</value>
<remarks>
<para>
This property corresponds to the "Connection Idle Timeout" key (synonym: "Pool Idle Timeout") within the connection string.
</para>
<para>
When a caller retrieves a connection from the pool, the driver checks how long the connection has been sitting idle. If the idle duration exceeds the value of <c>Connection Idle Timeout</c>, the connection is discarded and a different valid or newly-created connection is returned instead. This protects callers from receiving connections that may have been silently closed by firewalls, load balancers, or server-side inactivity thresholds.
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
</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.
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
</para>
Comment thread
priyankatiwari08 marked this conversation as resolved.
Comment on lines +989 to +1004
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

rightly pointed. Added in document

</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,7 @@ 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
internal const int IdleTimeout = 0; // default of 0 means don't expire idle connections
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
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";
Comment thread
paulmedynski marked this conversation as resolved.
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 @@ -32,6 +32,7 @@ internal static class DbConnectionStringSynonyms
internal const string PacketSize = "packetsize";
internal const string PersistSecurityInfo = "persistsecurityinfo";
internal const string PoolBlockingPeriod = "poolblockingperiod";
internal const string PoolIdleTimeout = "pool idle timeout";
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
internal const string Pwd = "pwd";
internal const string Server = "server";
internal const string ServerCertificate = "servercertificate";
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,13 @@ 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.
/// Used by the pool to discard connections that have sat unused longer than the configured idle timeout.
/// </summary>
internal DateTime IdleSinceUtc { get; private set; }

/// <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 +746,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()
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
{
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,9 @@ 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.
connection.MarkPooledIdle();
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
var written = _idleChannel.TryWrite(connection);
Debug.Assert(written, "Failed to write returning connection to the idle channel.");
}
Expand Down Expand Up @@ -436,6 +439,15 @@ private bool IsLiveConnection(DbConnectionInternal connection)
return false;
}

// Connection has been sitting idle longer than the configured idle timeout.
// 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;
if (idleTimeout != TimeSpan.Zero && DateTime.UtcNow > connection.IdleSinceUtc + idleTimeout)
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
{
return false;
}

// Connection was created before the last Clear, so it's stale.
if (connection.ClearGeneration != _clearGeneration)
{
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 = 0
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
)
{
_poolByIdentity = poolByIdentity;
Expand All @@ -36,6 +38,11 @@ bool hasTransactionAffinity
_useLoadBalancing = true;
}

if (0 != idleTimeout)
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
{
_idleTimeout = new TimeSpan(0, 0, idleTimeout);
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
}
Comment thread
priyankatiwari08 marked this conversation as resolved.

_hasTransactionAffinity = hasTransactionAffinity;
}

Expand All @@ -54,6 +61,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 @@ -1028,7 +1028,7 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj
Interlocked.Decrement(ref _waitCount);
obj = GetFromGeneralPool();

if ((obj != null) && (!obj.IsConnectionAlive()))
if ((obj != null) && (!obj.IsConnectionAlive() || IsIdleExpired(obj)))
{
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetConnection|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
Expand Down Expand Up @@ -1207,7 +1207,7 @@ private DbConnectionInternal GetFromTransactedPool(out Transaction transaction)
throw;
}
}
else if (!obj.IsConnectionAlive())
else if (!obj.IsConnectionAlive() || IsIdleExpired(obj))
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
{
SqlClientEventSource.Log.TryPoolerTraceEvent("<prov.DbConnectionPool.GetFromTransactedPool|RES|CPOOL> {0}, Connection {1}, found dead and removed.", Id, obj.ObjectID);
DestroyObject(obj);
Expand Down Expand Up @@ -1329,13 +1329,27 @@ 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.
obj.MarkPooledIdle();
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
_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 idleTimeout != TimeSpan.Zero && DateTime.UtcNow > obj.IdleSinceUtc + idleTimeout;
}

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 @@ -731,7 +731,8 @@ private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(Sql
opt.MaxPoolSize,
connectionTimeout,
opt.LoadBalanceTimeout,
opt.Enlist);
opt.Enlist,
opt.IdleTimeout);
}
return poolingOptions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ internal static class TRANSACTIONBINDING
private readonly int _commandTimeout;
private readonly int _connectTimeout;
private readonly int _loadBalanceTimeout;
private readonly int _idleTimeout;
private readonly int _maxPoolSize;
private readonly int _minPoolSize;
private readonly int _packetSize;
Expand Down Expand Up @@ -195,6 +196,8 @@ static SqlConnectionOptions()
DbConnectionStringSynonyms.IpAddressPreference);
AddKeywordToMap(DbConnectionStringKeywords.LoadBalanceTimeout,
DbConnectionStringSynonyms.ConnectionLifetime);
AddKeywordToMap(DbConnectionStringKeywords.IdleTimeout,
DbConnectionStringSynonyms.PoolIdleTimeout);
AddKeywordToMap(DbConnectionStringKeywords.MultipleActiveResultSets,
Comment thread
priyankatiwari08 marked this conversation as resolved.
DbConnectionStringSynonyms.MultipleActiveResultSets);
AddKeywordToMap(DbConnectionStringKeywords.MaxPoolSize);
Expand Down Expand Up @@ -274,6 +277,7 @@ internal SqlConnectionOptions(string connectionString)
_commandTimeout = ConvertValueToInt32(DbConnectionStringKeywords.CommandTimeout, DbConnectionStringDefaults.CommandTimeout);
_connectTimeout = ConvertValueToInt32(DbConnectionStringKeywords.ConnectTimeout, DbConnectionStringDefaults.ConnectTimeout);
_loadBalanceTimeout = ConvertValueToInt32(DbConnectionStringKeywords.LoadBalanceTimeout, DbConnectionStringDefaults.LoadBalanceTimeout);
_idleTimeout = ConvertValueToInt32(DbConnectionStringKeywords.IdleTimeout, DbConnectionStringDefaults.IdleTimeout);
_maxPoolSize = ConvertValueToInt32(DbConnectionStringKeywords.MaxPoolSize, DbConnectionStringDefaults.MaxPoolSize);
_minPoolSize = ConvertValueToInt32(DbConnectionStringKeywords.MinPoolSize, DbConnectionStringDefaults.MinPoolSize);
_packetSize = ConvertValueToInt32(DbConnectionStringKeywords.PacketSize, DbConnectionStringDefaults.PacketSize);
Expand Down Expand Up @@ -318,6 +322,11 @@ internal SqlConnectionOptions(string connectionString)
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.LoadBalanceTimeout);
}

if (_idleTimeout < 0)
{
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.IdleTimeout);
}

if (_connectTimeout < 0)
{
throw ADP.InvalidConnectionOptionValue(DbConnectionStringKeywords.ConnectTimeout);
Expand Down Expand Up @@ -579,6 +588,7 @@ internal SqlConnectionOptions(SqlConnectionOptions connectionOptions, string dat
_commandTimeout = connectionOptions._commandTimeout;
_connectTimeout = connectionOptions._connectTimeout;
_loadBalanceTimeout = connectionOptions._loadBalanceTimeout;
_idleTimeout = connectionOptions._idleTimeout;
_poolBlockingPeriod = connectionOptions._poolBlockingPeriod;
_maxPoolSize = connectionOptions._maxPoolSize;
_minPoolSize = connectionOptions._minPoolSize;
Expand Down Expand Up @@ -650,6 +660,9 @@ internal SqlConnectionOptions(SqlConnectionOptions connectionOptions, string dat
internal int CommandTimeout => _commandTimeout;
internal int ConnectTimeout => _connectTimeout;
internal int LoadBalanceTimeout => _loadBalanceTimeout;
// Maximum time (in seconds) a connection can sit idle in the pool before it is discarded
// on the next retrieval attempt. 0 disables idle expiration.
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
internal int IdleTimeout => _idleTimeout;
internal int MaxPoolSize => _maxPoolSize;
internal int MinPoolSize => _minPoolSize;
internal int PacketSize => _packetSize;
Expand Down
Loading
Loading