A maior e melhor Black Friday que já fizemos! Descontos a partir de 50% em até 12x sem juros - Garanta seu Desconto

Connection pool do SqlClient: como ativar o Pool V2

Connection pool do SqlClient: como ativar o Pool V2

O connection pool do SqlClient ganhou uma nova implementação, chamada Pool V2, que abre conexões físicas em paralelo e reduz bastante o tempo de espera quando muitas requisições chegam ao mesmo tempo. Neste artigo, você entende por que o pool legado era um gargalo, como o Pool V2 funciona, quais números a Microsoft publicou e como ativar o recurso com uma única linha de código.

O que é o connection pool do SqlClient

Abrir uma conexão com o SQL Server é uma operação cara. Afinal, ela envolve conexão de rede, handshake TLS, autenticação e configuração da sessão.

Por isso, o connection pool do SqlClient mantém conexões físicas já autenticadas e as reutiliza entre operações. Quando o código chama Open ou OpenAsync, o driver procura uma conexão livre no pool em vez de criar outra do zero.

Da mesma forma, quando o código chama Close, Dispose ou DisposeAsync, a conexão física não termina. Na prática, o driver reinicia o estado da sessão e devolve essa conexão ao pool para o próximo uso.

O pooling vem habilitado por padrão no Microsoft.Data.SqlClient. Assim, o padrão recomendado continua simples: abra tarde, descarte cedo e deixe o pool cuidar das conexões físicas.

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);

await using var command = new SqlCommand("SELECT COUNT(*) FROM Orders", connection);
var totalOrders = (int)await command.ExecuteScalarAsync(cancellationToken);

Além disso, cada string de conexão distinta gera um pool próprio. Ou seja, duas strings com as mesmas configurações, mas com as palavras em ordem diferente, criam dois pools separados.

Por que o pool legado limita aberturas paralelas

O connection pool do SqlClient legado, agora chamado de V1, tem uma restrição importante. Dentro de um mesmo pool, ele abre apenas uma conexão nova por vez.

Em uma aplicação com tráfego estável, isso quase não aparece, porque o pool já está aquecido. No entanto, o cenário muda quando várias requisições precisam de conexões novas ao mesmo tempo.

Pense, por exemplo, em uma API que acabou de subir e recebe cem chamadas simultâneas. Nesse caso, o connection pool do SqlClient no modo V1 cria as conexões em fila, uma depois da outra, e cada chamada espera a anterior terminar.

O mesmo problema surge em situações bem comuns. Entre elas, estão a leitura de vários metadados durante o startup e um pico de tráfego depois de um período ocioso.

Com isso, a aplicação demora mais para ficar pronta e a latência das primeiras requisições sobe. Em ambientes com escala automática, esse atraso se repete a cada nova instância que entra em operação.

Os contornos que as equipes usavam

Para fugir desse gargalo, muitas equipes recorreram a soluções criativas. A mais comum era aumentar o Min Pool Size para reservar conexões antecipadamente.

Contudo, essa abordagem tem custo. Um Min Pool Size positivo mantém sessões abertas mesmo quando a aplicação está ociosa, o que vai contra cenários de scale to zero, bancos serverless com pausa automática e instâncias burstable.

Outra saída, mais radical, era desligar o pooling por completo. Nesse caso, cada operação pagava o custo integral de uma conexão nova, o que raramente compensa.

Segundo a Microsoft, o Pool V2 torna esses contornos desnecessários. Portanto, vale revisar configurações antigas depois de migrar.

Como o Pool V2 funciona por dentro

A principal mudança do Pool V2 no connection pool do SqlClient é permitir a criação de conexões de forma concorrente. Assim, quando várias requisições pedem conexões novas, o pool responde a todas em paralelo.

Além disso, a nova implementação nasceu pensando em cargas assíncronas. O núcleo dela usa System.Threading.Channels, um conjunto de estruturas de produtor e consumidor com suporte nativo a async.

Em vez de depender de threads dedicadas em segundo plano, o pool trabalha com canais. Dessa forma, aplicações que se conectam a muitos bancos diferentes gastam menos recursos gerenciando threads.

Vale notar também a origem desse desenho. A própria Microsoft reconhece que o time do driver Npgsql, usado com PostgreSQL, abriu o caminho para esse modelo de pooling.

Para quem nunca usou channels, o conceito é parecido com uma fila segura entre threads. Por exemplo, o trecho abaixo mostra a ideia geral de produtor e consumidor, sem relação direta com o código interno do driver.

using System.Threading.Channels;

// Canal limitado: quem escreve espera quando a fila enche
var channel = Channel.CreateBounded<string>(capacity: 10);

var producer = Task.Run(async () =>
{
    for (var i = 0; i < 20; i++)
        await channel.Writer.WriteAsync($"item-{i}");

    channel.Writer.Complete();
});

await foreach (var item in channel.Reader.ReadAllAsync())
    Console.WriteLine($"Consumed {item}");

await producer;

Na prática, essa estrutura permite que o connection pool do SqlClient entregue conexões a quem está esperando sem bloquear threads desnecessariamente.

Resultados dos benchmarks do connection pool do SqlClient

A Microsoft mediu quanto tempo cada implementação do connection pool do SqlClient leva para estabelecer um conjunto de conexões em um cold start. Em cada teste, o pool começava vazio e vários chamadores abriam e seguravam conexões até todas as aberturas terminarem.

O tempo medido inclui a criação das conexões, a sincronização e a devolução ao pool. Por outro lado, a limpeza do pool e a preparação do teste ficaram de fora.

Os testes usaram 10, 25, 50 e 100 chamadores concorrentes, sempre com Max Pool Size=200. Além disso, o SQL Server rodava na mesma máquina do benchmark para reduzir a variação de rede.

Chamadas síncronas

No modo síncrono, cada chamador usa uma thread dedicada e invoca SqlConnection.Open(). Então, cada um segura a conexão até todos terminarem.

Com 100 chamadores no Linux, o tempo médio caiu de 630,1 ms para 117,5 ms. Em outras palavras, o Pool V2 foi cerca de 5,4 vezes mais rápido que o pool legado.

Chamadas assíncronas

No modo assíncrono, os chamadores usam tasks com SqlConnection.OpenAsync(). O restante do cenário permanece igual.

Nesse caso, o ganho do novo connection pool do SqlClient foi ainda maior. Com 100 chamadores no Linux, o tempo médio caiu de 604,1 ms para 92,0 ms, uma aceleração de aproximadamente 6,6 vezes.

O ambiente Linux era uma VM Ubuntu 22.04.5 com 16 núcleos e 32 CPUs lógicas, SQL Server 2022 Developer CU26, .NET 9.0.19 e BenchmarkDotNet 0.15.8. Já o ambiente Windows usava Windows Server 2022 Datacenter Azure Edition, com SQL Server 2025 Enterprise Evaluation.

Todo o código do benchmark está publicado no repositório oficial do SqlClient no GitHub. Assim, você pode reproduzir os testes no seu próprio hardware.

O que os números não medem

Antes de comemorar, vale entender o escopo desses resultados. Os testes medem apenas o estabelecimento de conexões em um pool frio e a coordenação entre os chamadores.

Ou seja, eles não medem execução de consultas nem reutilização de conexões em um pool já aquecido. Os valores também representam o tempo médio da operação completa, e não a latência de uma abertura individual.

Além disso, o banco estava na mesma máquina. Portanto, em produção, com latência de rede real, os números vão variar conforme a carga e a distância até o servidor.

Como ativar o Pool V2 no connection pool do SqlClient

O Pool V2 está disponível a partir da versão 7.1.0 do pacote Microsoft.Data.SqlClient. Primeiro, atualize a dependência do projeto.

dotnet add package Microsoft.Data.SqlClient --version 7.1.1

Se o projeto também referencia pacotes complementares, como Microsoft.Data.SqlClient.Extensions.Azure, atualize todos para a mesma versão. Desde a versão 7.0.2, o driver e esses pacotes seguem versionamento alinhado.

Em seguida, ative o recurso com um switch do AppContext. O V1 continua sendo o padrão, então o connection pool do SqlClient só usa o V2 quando você liga esse switch explicitamente.

AppContext.SetSwitch(
    "Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2",
    true);

O detalhe mais importante é o momento da chamada. Segundo a documentação de switches do AppContext no Microsoft Learn, o SqlClient lê e guarda cada switch na primeira vez que o usa.

Por isso, alterar o valor depois que o driver já abriu uma conexão não tem efeito. Coloque a chamada no início do ponto de entrada da aplicação, antes de qualquer serviço que acesse o banco.

Ativando em uma aplicação ASP.NET Core

Em uma aplicação ASP.NET Core, o switch precisa vir antes da criação do builder. Veja um exemplo com minimal APIs.

using Microsoft.Data.SqlClient;

// Precisa vir antes de qualquer acesso ao banco
AppContext.SetSwitch(
    "Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2",
    true);

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("Default")
    ?? throw new InvalidOperationException("Connection string 'Default' not found.");

var app = builder.Build();

app.MapGet("/orders/count", async (CancellationToken cancellationToken) =>
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync(cancellationToken);

    await using var command = new SqlCommand("SELECT COUNT(*) FROM Orders", connection);
    var total = (int)await command.ExecuteScalarAsync(cancellationToken);

    return Results.Ok(new { total });
});

app.Run();

Essa é a única alteração de código necessária. Suas strings de conexão e o restante do acesso a dados permanecem como estão.

Se preferir não mexer no código, também é possível declarar o switch no arquivo de projeto. Dessa forma, o valor entra no runtimeconfig.json durante o build.

<ItemGroup>
  <RuntimeHostConfigurationOption Include="Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"
                                  Value="true" />
</ItemGroup>

Para voltar ao pool legado, basta definir o switch como false e reiniciar a aplicação.

Uso com EF Core e Dapper

Como o Pool V2 vive dentro do driver, ele funciona com qualquer biblioteca que use o Microsoft.Data.SqlClient por baixo. Isso inclui o EF Core e o Dapper.

No EF Core, por exemplo, o registro do DbContext não muda. O provider de SQL Server abre as conexões pelo SqlClient, então o connection pool do SqlClient aplica o V2 automaticamente.

AppContext.SetSwitch(
    "Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2",
    true);

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

Com o Dapper, a lógica é a mesma. Você continua criando um SqlConnection e chamando os métodos de extensão normalmente.

await using var connection = new SqlConnection(connectionString);

var customers = await connection.QueryAsync<Customer>(
    "SELECT Id, Name, Email FROM Customers WHERE IsActive = @IsActive",
    new { IsActive = true });

Limitação atual do OpenAsync

Apesar dos ganhos do connection pool do SqlClient, existe um ponto de atenção. O SqlConnection.OpenAsync() ainda não é totalmente assíncrono em cada chamada de rede individual.

Hoje, o Pool V2 enfileira trabalho em threads do thread pool gerenciado, e essas threads fazem chamadas de rede síncronas. Enquanto a rede responde rápido, isso quase não pesa.

No entanto, se a latência até o SQL Server for alta, essas threads ficam presas esperando. Consequentemente, o thread pool sofre pressão e precisa criar threads novas, o que adiciona latência para o usuário final.

A Microsoft já informou que trabalhos futuros vão corrigir essas chamadas de rede. Até lá, aplicações distantes do banco precisam de cuidado extra.

Mitigando a pressão no connection pool do SqlClient

Para proteger o connection pool do SqlClient, a primeira recomendação é monitorar o thread pool gerenciado. Por exemplo, a ferramenta dotnet-counters mostra a contagem de threads e o tamanho da fila em tempo real.

dotnet-counters monitor --process-id 12345 --counters System.Runtime

Se a fila crescer durante picos, uma opção é aumentar o número mínimo de threads. Assim, o runtime não precisa esperar o algoritmo de injeção de threads para atender a demanda.

ThreadPool.GetMinThreads(out var workerThreads, out var ioThreads);

// Ajuste com base em medições reais, nunca por chute
ThreadPool.SetMinThreads(Math.Max(workerThreads, 64), ioThreads);

Outra opção é limitar a concorrência das operações de banco. Com um SemaphoreSlim, você controla quantas chamadas acessam o banco ao mesmo tempo.

public sealed class DatabaseGate(int maxConcurrency)
{
    private readonly SemaphoreSlim _semaphore = new(maxConcurrency);

    public async Task<T> RunAsync<T>(
        Func<CancellationToken, Task<T>> operation,
        CancellationToken cancellationToken)
    {
        await _semaphore.WaitAsync(cancellationToken);
        try
        {
            return await operation(cancellationToken);
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

Na borda da API, também vale controlar a entrada de requisições. Nesse sentido, o artigo sobre rate limiting no ASP.NET Core 10 mostra como aplicar limitadores de concorrência e evitar sobrecarga.

Configurações do connection pool do SqlClient que continuam valendo

As palavras-chave de pooling da string de conexão valem para as duas implementações. Portanto, tudo o que você já configurou continua funcionando no V2.

  • Pooling: liga ou desliga o pooling e vem como true por padrão.
  • Min Pool Size: define o mínimo de conexões mantidas e vem como 0.
  • Max Pool Size: limita as conexões físicas de cada pool e vem como 100.
  • Connect Timeout: define quanto tempo a abertura espera e vem como 15 segundos.
  • Connection Idle Timeout: chegou na versão 7.1.0 e torna elegíveis para remoção as conexões ociosas após 300 segundos.

O Connection Idle Timeout exige um switch adicional para entrar em vigor. Nesse caso, é preciso definir Switch.Microsoft.Data.SqlClient.UseLegacyIdleTimeoutBehavior como false.

A versão 7.1.0 também trouxe o switch UseOverallConnectTimeoutForPoolWait. Com ele ativo, a espera no pool e a tentativa de rede passam a dividir um único orçamento de Connect Timeout.

AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2", true);
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseLegacyIdleTimeoutBehavior", false);
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseOverallConnectTimeoutForPoolWait", true);

var builder = new SqlConnectionStringBuilder(connectionString)
{
    MaxPoolSize = 200,
    IdleTimeout = 120
};

Antes de aumentar o Max Pool Size, confira se todas as conexões e readers são descartados em todos os caminhos. Além disso, calcule o total multiplicando o limite pelo número de instâncias da aplicação, porque cada processo tem seu próprio pool.

Por fim, evite fragmentar o pool. Monte uma única string de conexão canônica com SqlConnectionStringBuilder e reutilize a mesma instância de callback de token ou de credencial.

Monitorando o connection pool do SqlClient em produção

O connection pool do SqlClient expõe contadores de diagnóstico que ajudam a validar a migração. Entre eles, estão hard connects, soft connects, conexões ativas e livres e conexões recuperadas sem descarte.

Compare esses contadores antes e depois de ativar o V2. Desse modo, você confirma se o tempo de abertura caiu de fato no seu cenário.

Se aparecerem timeouts, não culpe o pool de imediato. Afinal, um timeout pode indicar vazamento de conexão, consulta lenta, transação bloqueada ou limite de capacidade do banco.

Quando avaliar o Pool V2

A Microsoft recomenda testar o Pool V2 do connection pool do SqlClient com uma carga representativa. Em especial, os cenários abaixo tendem a mostrar mais diferença.

  • Startup da aplicação, quando o pool está vazio.
  • Picos de tráfego depois de um período ocioso.
  • Alta demanda concorrente por conexões.
  • Conexões com latência mais alta até o SQL Server.

Note que o último item também é o ponto sensível da limitação do OpenAsync. Por isso, nesse cenário, meça o thread pool com atenção durante o teste.

Uma estratégia segura é ativar o novo connection pool do SqlClient por configuração e liberar aos poucos. Assim, se algo sair do esperado, você reverte sem novo deploy.

var useConnectionPoolV2 = Environment.GetEnvironmentVariable("USE_SQLCLIENT_POOL_V2") == "true";

AppContext.SetSwitch(
    "Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2",
    useConnectionPoolV2);

Combine essa abordagem com políticas de resiliência na aplicação. Nesse ponto, o artigo sobre Polly no .NET ajuda a configurar retry com limites, sem gerar tempestades de reconexão.

A Microsoft também pretende tornar o Pool V2 o padrão em uma versão futura do driver. Então, testar agora antecipa possíveis surpresas e ainda gera feedback útil para o time do SqlClient.

Conclusão sobre o connection pool do SqlClient

O Pool V2 resolve uma limitação antiga do connection pool do SqlClient, que criava apenas uma conexão nova por vez dentro de cada pool. Com a abertura concorrente, aplicações que sobem frias ou recebem picos de tráfego passam a ficar prontas bem mais rápido.

Os benchmarks oficiais mostram ganhos de 5,4 a 6,6 vezes no cold start com 100 chamadores. Mesmo assim, lembre que esses testes rodaram com o banco local e não medem consultas.

A ativação exige apenas um switch do AppContext no início da aplicação. Além disso, EF Core, Dapper e qualquer código ADO.NET passam a usar o novo pool sem mudanças adicionais.

Por outro lado, o OpenAsync ainda faz chamadas de rede síncronas por baixo. Portanto, em ambientes com alta latência, monitore o thread pool e limite a concorrência quando necessário.

Em resumo, vale testar o Pool V2 agora em um ambiente de homologação. Dessa forma, quando ele virar o padrão do connection pool do SqlClient, sua aplicação já estará validada.

FAQ

Qual versão do Microsoft.Data.SqlClient traz o Pool V2?

O Pool V2 está disponível a partir da versão 7.1.0 do pacote. Ele vem desligado por padrão e exige o switch UseConnectionPoolV2.

Preciso mudar minha string de conexão para usar o Pool V2?

Não. As strings de conexão e o código de acesso a dados permanecem iguais, e as palavras-chave de pooling valem para as duas implementações.

O Pool V2 funciona com EF Core?

Sim. Como o provider de SQL Server do EF Core usa o Microsoft.Data.SqlClient, o novo pool entra em ação automaticamente.

Posso ativar o switch depois que a aplicação já abriu conexões?

Não adianta. O SqlClient guarda o valor do switch no primeiro uso, então a chamada precisa acontecer antes de qualquer acesso ao banco.

Como volto para o pool legado?

Defina o switch como false e reinicie a aplicação. Nenhuma outra alteração é necessária.

Ainda vale a pena usar Min Pool Size alto?

Na maioria dos casos, não. O Pool V2 abre conexões em paralelo, o que elimina o principal motivo para reservar conexões antecipadamente.