Skip to content

Managed SNI on Windows: every pooled open over TCP waits ~15 ms in SniTcpHandle.CheckConnection #5070

Open

Description

@RoccoPanunzio

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.

Activity

  1. moved this from To triage to In progress in SqlClient Boardon Nov 14, 2026
  2. github-actions commented on Nov 14, 2026

    @github-actions

    🔍 Triage Summary

    Check Result
    Issue type Bug
    Environment All required environment details provided for investigation
    Area Area\Managed SNI
    Duplicates None found (related only by component: #5045, #5022)
    Regression Not indicated

    Analysis

    Under managed SNI on Windows, SniTcpHandle.CheckConnection calls Socket.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

    Note: This triage summary is auto-generated by an AI agent. The analysis and suggestions above have not been verified by a human maintainer. Please treat as preliminary guidance only.

    Generated by SqlClient Issue Auto-Triage for #5070 · copilot · auto · 22.9 AIC · ⌖ 11.3 AIC · ⊞ 7.8K · ◷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions