Muitos desenvolvedores que começam no ecossistema .NET têm receio de que ao abrir e fechar conexões com o banco com frequência prejudique a performance da aplicação. Esse medo geralmente se baseia na premissa de que criar uma conexão física é um processo custoso em termos de recursos computacionais. O ponto é que, no desenvolvimento moderno com C#, raramente estamos manipulando a conexão física. Existe um mecanismo eficiente entre o nosso código e o servidor de banco de dados: o Connection Pooling.
O Connection Pooling funciona como um reservatório de conexões físicas que permanecem ativas e disponíveis para o uso. Quando você instancia um objeto de conexão e solicita sua abertura, o framework não cria necessariamente um novo canal de comunicação com o banco. Em vez disso, ele solicita ao pool uma conexão disponível, em idle. Da mesma forma, ao sair do escopo de execução ou descartar o objeto, a conexão física não é destruída. Ela é apenas devolvida ao pool para que a próxima requisição possa utilizá-la com o menor esforço computacional possível..
Para o pool funcionar bem, o desenvolvedor deve seguir a estratégia de manter o escopo da conexão o mais restrito possível, seguindo o princípio Open Late, Close Early (A ideia é abrir a conexão o mais tarde possível e descartá-la assim que a operação terminar). Veja abaixo um exemplo de como implementar esse padrão de forma eficiente:
public async Task<IEnumerable<Something>> GetAsync(CancellationToken cancellationToken)
{
var sql = @"<MY_SQL_CODE>";
await using SqlConnection connection = new(_connectionString);
return await connection
.QueryAsync<Something>(sql);
}
No exemplo acima, o uso de await using pega uma conexão do pool só na hora da execução. Quando a consulta termina, a conexão volta para o pool imediatamente, mesmo que algo dê errado no caminho (alguma exception). Ao declarar a conexão dentro do método e não como um campo de classe ou um objeto persistente, garantimos que o recurso não fique retido desnecessariamente. Esse padrão no código é o que permite que o pool suporte milhares de requisições utilizando um número relativamente pequeno de conexões físicas.
Entretanto, é fundamental compreender a distinção entre instanciar o objeto e efetivamente abrir a conexão, pois o recurso do pool só é consumido no momento da abertura. O cenário torna-se problemático quando ocorrem os chamados vazamentos de conexão (connection leaks), onde o desenvolvedor não descarta o objeto, impedindo que o recurso retorne ao reservatório. Da mesma forma, manter transações longas que englobam regras de negócio pesadas ou chamadas externas sequestra a conexão por tempo excessivo. Nesses casos, o pool fica saturado e a aplicação começa a apresentar lentidão não pela execução das queries em si, mas pela fila de espera para obter uma conexão disponível, resultando em erros de timeout.
Tentar “otimizar” esse processo manualmente, como criar conexões no construtor de uma classe ou injetá-las como instâncias únicas (Singleton), é uma prática perigosa. Esse tipo de abordagem impede que a conexão retorne ao pool, causando saturação e levando a erros de timeout. É comum encontrar times que aumentam o limite máximo de conexões do pool (via connection string) para resolver problemas de falta de conexões. Contudo, o problema pode não ser exaustão de recursos, mas sim o fato de as conexões estarem sendo “presas” por longos períodos em vez de serem liberadas o mais cedo possível.
Confiar no gerenciamento do .NET e utilizar corretamente os mecanismos de descarte de objetos é a abordagem correta para sustentar a escalabilidade. O objetivo não deve ser reaproveitar o objeto de conexão, mas sim garantir que ele seja devolvido ao pool o mais rápido possível para que o framework faça o reaproveitamento da conexão física nos bastidores.
Insights & Takeaways
- Instanciar um SqlConnection não cria uma nova conexão física, mas solicita um recurso já existente no pool gerenciado pelo .NET.
- A recomendação fundamental é abrir a conexão o mais tarde possível e garantir seu descarte o mais cedo possível para evitar a saturação do reservatório.
- O uso de await using no escopo do método é a forma mais eficiente de garantir que a conexão retorne ao pool imediatamente após o uso.
- Aumentar o tamanho máximo do pool geralmente esconde falhas de gestão de recursos no código, como conexões mantidas abertas por tempo excessivo.
- Evite injetar conexões com ciclos de vida longos, pois isso impede a rotatividade adequada de recursos que o Connection Pooling se propõe a gerenciar.