Repository navigation
Description
Describe the bug
With the managed network layer on Windows (Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows),
each SqlConnection.Open() served from the pool over TCP takes about 15 ms when the connection has been
idle for more than 5 ms. The native layer takes about 0.2 ms for the same open.
The time is spent in SniTcpHandle.CheckConnection:
if (!_socket.Connected || (_socket.Poll(100, SelectMode.SelectRead) && _socket.Available == 0))On a healthy, idle connection nothing is readable, so Poll waits its whole timeout. Windows rounds the
100 µs up to the system timer tick (15.6 ms by default), so the check costs a full tick. It runs whenever
the last I/O is older than CheckConnectionWindow (5 ms), so an application that opens a pooled connection
for each short piece of work pays it every time.
The wait does not help to detect a broken connection: a connection closed by the peer is already readable,
and Poll(0, SelectMode.SelectRead) reports it at once. The check used Poll(0, SelectMode.SelectError)
until dotnet/corefx#31631, which moved to SelectRead to fix detection on Linux and set the timeout to
100 µs. The timeout is still 100 µs on main.
To reproduce
SQL Server reachable over TCP on localhost, Windows authentication:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.1.1" />
</ItemGroup>
</Project>using System.Diagnostics;
using Microsoft.Data.SqlClient;
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows", args.Contains("managed"));
const string ConnectionString = "Server=tcp:localhost,1433;Integrated Security=true;TrustServerCertificate=true;Encrypt=false";
const int Opens = 200;
using (SqlConnection warm = new(ConnectionString))
{
warm.Open();
}
double[] times = new double[Opens];
for (int i = 0; i < Opens; i++)
{
Thread.Sleep(10); // older than CheckConnectionWindow, so the pool checks the connection
long start = Stopwatch.GetTimestamp();
using SqlConnection connection = new(ConnectionString);
connection.Open();
times[i] = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
}
Array.Sort(times);
Console.WriteLine($"median {times[Opens / 2]:F2} ms, p90 {times[Opens * 9 / 10]:F2} ms");Run with dotnet run (native) and dotnet run -- managed:
| Network layer | Median open from the pool | p90 |
|---|---|---|
| Native | 0.19 ms | 0.34 ms |
| Managed | 15.2 ms | 15.7 ms |
Expected behavior
An open from the pool with the managed layer costs about the same as with the native one.
Further technical details
Microsoft.Data.SqlClient version: 7.1.1, and main at 5f74e8f
.NET target: .NET 10.0
SQL Server version: SQL Server 2025
Operating system: Windows 11 Pro
Additional context
The tick only applies while no process has raised the timer resolution (timeBeginPeriod); with it at 1 ms
the check costs 1 ms instead. I have a fix with tests ready and will open a pull request.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn progress
Activity
github-actions commentedon Nov 14, 2026
🔍 Triage Summary
Area\Managed SNIAnalysis
Under managed SNI on Windows,
SniTcpHandle.CheckConnectioncallsSocket.Poll(100, SelectRead)on idle pooled connections. Windows rounds the 100 μs wait up to the timer tick, so every pooled open costs ~15 ms versus ~0.2 ms with native SNI. A peer-closed connection is already readable, so a zero timeout should detect it. This is a performance bug, roughly P2.Next Steps
Poll(0, SelectRead)still detects broken connections on Windows and Linux, which was the reason for the earlier change in [Fix] GH 30738 SqlConnection error when executing query after SQL server killed connections(Linux) corefx#31631.