← Voltar para Projetos e Artigos

Pool de Conexões Esgotado

Caso de estudo e runbook: como uma tela que respondia em 9 ms passou a estourar timeout de 30 segundos em um único cliente de um sistema multi-tenant — e como diagnosticar, tratar e validar isso em três passos no SQL Server.
SQL Server Pool de Conexões Multi-tenant .NET / ADO.NET DMVs Troubleshooting Runbook

Caso real de produção · Diagnóstico e procedimento de contenção · Documento de referência

O que você vai encontrar aqui

  1. O caso em uma frase
  2. Conceito — o que é um pool de conexões
  3. Passo 1 — Identificar (diagnóstico sem risco)
  4. Passo 2 — Encerrar as sessões órfãs
  5. Passo 3 — Validar o resultado
  6. Runbook — versão para copiar e colar
  7. Lições do caso
  8. Glossário de bolso

1. O caso em uma frase

Uma tela de calendário parou de carregar para um único cliente de um sistema multi-tenant. Todos os outros tenants, rodando o mesmo código na mesma instância de SQL Server, seguiam normais.

Os quatro fatos que orientaram o diagnóstico

Fato observadoO que ele elimina
A query executada direto no SSMS respondeu em 9 ms (3 ms de CPU)Elimina query lenta, índice faltando e estatística desatualizada
A base do cliente era menor que a base de teste (8 linhas na tabela principal)Elimina volume de dados como causa
A lentidão era intermitente e terminava sempre em timeout de 30 sIndica espera por recurso, não processamento lento
Todas as telas daquele cliente travavam, não apenas umaElimina um endpoint específico — aponta para recurso compartilhado
A pergunta-chave: se a consulta leva 9 ms, o que consome os outros 29,99 segundos? A resposta não estava na execução da query. Estava antes dela — na fila para conseguir uma conexão com o banco.

A contagem de sessões abertas revelou o número que fechou o diagnóstico: 187 conexões ativas na base daquele cliente, todas ociosas (sleeping), para um sistema com 2 ou 3 usuários simultâneos.

Bloco 2

Conceito — o que é um pool de conexões

Definição

Abrir uma conexão com um banco de dados é uma operação cara: envolve handshake de rede, autenticação e alocação de recursos no servidor. Para evitar pagar esse custo a cada requisição, o ADO.NET mantém um pool — um conjunto de conexões já abertas e prontas para reuso.

Quando a aplicação "abre" uma conexão, ela na verdade pega emprestada uma do pool. Quando "fecha", ela devolve a conexão ao pool, que a mantém viva para a próxima requisição.

Analogia — o chaveiro das salas de reunião: imagine uma empresa com 100 chaves de sala na recepção. Quem precisa de uma sala pega uma chave, usa e devolve. Como as chaves circulam, 100 chaves atendem centenas de pessoas por dia sem problema. Agora imagine que algumas pessoas saem da sala e levam a chave embora. No começo ninguém nota. Mas conforme as chaves somem, chega o dia em que a recepção não tem mais nenhuma. Quem chega fica esperando no balcão por 30 segundos, desiste e vai embora reclamando que "a empresa está lenta" — quando na verdade as salas estão todas vazias.

Essa é exatamente a situação do caso: as salas estavam vazias (o banco respondia em 9 ms), mas não havia chave disponível (o pool estava esgotado por conexões que ninguém devolveu).

App / API nova requisição (IIS) espera Pool de Conexões Max Pool Size = 100 todas ocupadas — 0 livres SQL Server query: 9 ms CPU: 3 ms ocioso, saudável O gargalo não está no banco — está na fila 187 sessões abertas · 2 usuários reais
O banco responde em milissegundos. O tempo é gasto esperando uma conexão livre que nunca aparece.

Por que só um cliente foi afetado

Este é o detalhe que torna o caso didático. O ADO.NET mantém um pool separado para cada connection string distinta. Em um sistema multi-tenant onde cada cliente tem seu próprio banco, cada cliente tem também o seu próprio pool, isolado dos demais.

Analogia — chaveiros separados: cada tenant tem seu próprio chaveiro na recepção. Se as chaves do cliente A somem, o cliente B nem fica sabendo — o chaveiro dele continua completo. É por isso que o problema aparece "direcionado" a um cliente só, mesmo com todos rodando o mesmo código.
Causa raiz vs. sintoma: o pool esgotado é o sintoma. A causa raiz é o vazamento de conexão — código que pega a conexão emprestada e não devolve (um IDataReader ou SqlConnection sem Close(), Dispose() ou using). Os três passos a seguir tratam o sintoma e devolvem o sistema ao ar. A correção definitiva é no código.
⚡ Resumo rápido
Bloco 3

Passo 1 — Identificar (diagnóstico sem risco)

Objetivo

Listar exatamente quais sessões seriam encerradas, sem encerrar nenhuma. Este passo é apenas leitura: pode ser rodado em produção, a qualquer hora, sem efeito colateral.

-- Passo 1: apenas LISTA as sessões candidatas. Não altera nada.
SELECT
    s.session_id,
    s.status,
    s.open_transaction_count,
    DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) AS min_ocioso,
    s.host_name,
    s.program_name,
    s.login_time,
    'KILL ' + CAST(s.session_id AS VARCHAR) + ';' AS comando
FROM sys.dm_exec_sessions s
WHERE DB_NAME(s.database_id) = 'NOME_DA_BASE'      -- 1. só a base afetada
  AND s.is_user_process = 1                          -- 2. ignora processos internos
  AND s.session_id <> @@SPID                          -- 3. nunca a própria sessão
  AND s.status = 'sleeping'                         -- 4. só as que não executam nada
  AND s.open_transaction_count = 0                  -- 5. só as sem transação aberta
  AND s.program_name LIKE '%SqlClient%'            -- 6. só a aplicação, não o SSMS
  AND DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) >= 5  -- 7. ociosas há 5 min+
ORDER BY min_ocioso DESC;

As sete travas de segurança

Cada filtro do WHERE existe por um motivo. Entender o porquê de cada um é o que separa "rodar um script da internet" de "saber o que se está fazendo".

#FiltroPor que existe
1DB_NAME(...) = 'base'Garante que nenhum outro tenant da instância seja tocado. É o filtro mais importante em ambiente compartilhado.
2is_user_process = 1Exclui as sessões internas do próprio SQL Server, que nunca devem ser encerradas.
3session_id <> @@SPIDEvita que o script mate a própria sessão que o está executando.
4status = 'sleeping'Sessão dormindo não está processando nada. Encerrar uma sessão em execução poderia interromper uma operação legítima em andamento.
5open_transaction_count = 0A trava mais crítica. Matar sessão com transação aberta dispara rollback, que pode demorar e travar ainda mais o banco.
6program_name LIKE '%SqlClient%'Restringe às conexões vindas da aplicação .NET, preservando SSMS, jobs e ferramentas de terceiros.
7min_ocioso >= 5Uma conexão ociosa há segundos pode estar entre duas operações de um usuário ativo. Cinco minutos indica abandono real.
Boa prática: a coluna comando monta o KILL pronto de cada sessão. Isso permite copiar e executar manualmente uma ou outra, caso você prefira agir cirurgicamente em vez de rodar o loop do Passo 2.
Antes de seguir: confira a contagem retornada. Se vier um número muito diferente do esperado — especialmente se vier baixo — o problema pode não ser pool. Rodar o Passo 2 sem conferir o Passo 1 é agir às cegas.
Bloco 4

Passo 2 — Encerrar as sessões órfãs

Objetivo

Executar KILL em cada sessão listada no Passo 1. O KILL encerra a sessão do lado do SQL Server, liberando o slot correspondente.

Por que um cursor e não um comando único: o KILL do SQL Server não aceita subconsulta nem lista — ele exige um número literal de sessão por vez (KILL 53). Por isso é preciso iterar. O cursor é declarado como LOCAL FAST_FORWARD, que é a variação mais leve, somente leitura e de sentido único.

-- Passo 2: encerra as sessões. Use os MESMOS filtros do Passo 1.
DECLARE @spid INT, @sql NVARCHAR(100), @mortas INT = 0;

DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT s.session_id
    FROM sys.dm_exec_sessions s
    WHERE DB_NAME(s.database_id) = 'NOME_DA_BASE'
      AND s.is_user_process = 1
      AND s.session_id <> @@SPID
      AND s.status = 'sleeping'
      AND s.open_transaction_count = 0
      AND s.program_name LIKE '%SqlClient%'
      AND DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) >= 5;

OPEN cur;
FETCH NEXT FROM cur INTO @spid;

WHILE @@FETCH_STATUS = 0
BEGIN
    BEGIN TRY
        SET @sql = N'KILL ' + CAST(@spid AS NVARCHAR(10));
        EXEC sp_executesql @sql;
        SET @mortas = @mortas + 1;
    END TRY
    BEGIN CATCH
        PRINT 'Falhou no SPID ' + CAST(@spid AS VARCHAR) + ': ' + ERROR_MESSAGE();
    END CATCH

    FETCH NEXT FROM cur INTO @spid;
END

CLOSE cur;
DEALLOCATE cur;

PRINT 'Sessoes encerradas: ' + CAST(@mortas AS VARCHAR);
Por que o TRY/CATCH dentro do loop: entre o momento em que o cursor leu a lista e o momento do KILL, uma sessão pode ter encerrado sozinha. Sem o tratamento, o erro abortaria o script no meio, deixando o restante das sessões vivas. Com ele, a falha é registrada e o loop continua.
O que acontece do lado da aplicação: o pool do ADO.NET não percebe imediatamente que as conexões morreram — ele continua achando que tem 187 chaves. Mas na primeira vez que tenta entregar uma dessas conexões, o provider detecta que ela está morta, descarta e abre uma nova. Na prática, os slots vão sendo liberados nos primeiros segundos após o KILL. Pode haver alguns erros transitórios nesse intervalo.
Bloco 5

Passo 3 — Validar o resultado

Objetivo

Confirmar numericamente que as conexões foram liberadas, e confirmar funcionalmente que a aplicação voltou ao normal.

-- Passo 3: confere quantas conexões restaram na base
SELECT COUNT(*) AS conexoes
FROM sys.dm_exec_sessions
WHERE DB_NAME(database_id) = 'NOME_DA_BASE'
  AND program_name LIKE '%SqlClient%';

Como interpretar o número

ResultadoLeituraAção
Caiu para um punhado (menos de 10)Conexões liberadas com sucesso. Diagnóstico confirmado.Testar a aplicação e registrar os números
Caiu, mas a lentidão continuaO gargalo está no pool do lado da aplicação, não no servidorReciclar o application pool no IIS
Continua altoOs filtros não casaram, ou as conexões são recriadas imediatamenteRevisar o nome da base e o program_name no Passo 1
Validação funcional: o número no SQL Server é metade da prova. A outra metade é abrir a tela afetada no aplicativo e confirmar que ela carrega. Só então o procedimento está concluído.

O que registrar para o histórico

Esse registro serve a dois propósitos: comprovar o diagnóstico e medir o ritmo de acúmulo. Se o número voltar a 187 em três dias, você sabe que tem até três dias para corrigir o código.

Bloco 6

Runbook — versão para copiar e colar

Antes de executar: substitua NOME_DA_BASE pelo nome real do banco em todos os três passos. Execute sempre na ordem 1 → 2 → 3, e nunca pule o Passo 1.

Sequência de execução

OrdemPassoRiscoTempo
1Listar candidatasNenhum — somente leitura< 1 s
2Encerrar sessõesBaixo — filtros protegem sessões ativasSegundos
3Validar contagemNenhum — somente leitura< 1 s
4Testar a tela no appNenhum1 min

Plano B — reciclar o application pool

Se após o Passo 2 a contagem caiu mas a lentidão persistiu, o pool preso está do lado do servidor de aplicação. O comando abaixo recicla o processo do IIS:

:: Executar no servidor de aplicação, com privilégio administrativo
%windir%\system32\inetsrv\appcmd recycle apppool /apppool.name:"NOME_DO_APP_POOL"
Impacto: o reciclo do application pool derruba todos os tenants servidos por aquele pool durante alguns segundos. Sempre que possível, avise antes ou execute fora do horário comercial. É a opção mais invasiva das duas.
Bloco 7

Lições do caso

1. "Está lento" quase nunca significa "está lento"

Uma operação que leva 9 ms e estoura em 30 s não está devagar — está parada, esperando algum recurso. Lentidão gradual e travamento por espera têm causas e soluções completamente diferentes. A primeira pergunta do diagnóstico deve ser sempre: o tempo está sendo gasto ou esperado?

2. Medir antes de otimizar

A hipótese inicial natural era query lenta na base do cliente. Um SET STATISTICS IO, TIME ON derrubou essa hipótese em 30 segundos e evitou horas de trabalho em índices que não resolveriam nada.

3. O padrão do problema conta a história

Padrão observadoO que ele sugere
Uma tela só, sempre lentaQuery, índice ou volume de dados
Todas as telas, intermitente, um tenant sóRecurso compartilhado por tenant — pool ou conexão
Todas as telas, todos os tenantsInfraestrutura, rede ou o servidor como um todo

4. Paliativo é legítimo, desde que seja nomeado como tal

Este procedimento não corrige a causa raiz. As conexões voltarão a acumular no mesmo ritmo, porque o código que as vaza continua o mesmo. O KILL compra tempo — a correção definitiva é garantir que todo IDataReader e toda SqlConnection sejam fechados em um bloco using ou finally. Enquanto isso não acontece, vale agendar um reciclo diário do application pool na madrugada para conter o acúmulo.
⚡ Resumo rápido do caso

Glossário de bolso

TermoSignificado
Pool de conexõesConjunto de conexões de banco já abertas e mantidas pelo ADO.NET para reuso entre requisições
Max Pool SizeLimite de conexões simultâneas de um pool. Padrão do .NET: 100
Vazamento de conexãoConexão emprestada do pool e nunca devolvida, por falta de Close(), Dispose() ou using
Sessão sleepingConexão aberta no SQL Server que não está executando comando algum no momento
SPIDServer Process ID — identificador numérico de uma sessão no SQL Server
@@SPIDVariável de sistema que retorna o SPID da sessão que está executando o comando
DMVDynamic Management View — visões de sistema que expõem o estado interno do SQL Server
sys.dm_exec_sessionsDMV que lista todas as sessões conectadas, com status, banco, aplicação e transações abertas
KILLComando que encerra uma sessão no SQL Server. Aceita apenas um SPID literal por vez
Cursor FAST_FORWARDCursor somente leitura e de sentido único — a variação mais leve do SQL Server
open_transaction_countNúmero de transações abertas em uma sessão. Valor maior que zero exige cautela antes de qualquer KILL
Application pool (IIS)Processo isolado do IIS que hospeda uma ou mais aplicações web
RollbackDesfazimento de uma transação. Disparado automaticamente ao encerrar uma sessão com transação aberta
Multi-tenantArquitetura em que uma mesma aplicação atende vários clientes com isolamento de dados entre eles

Caso de estudo — Pool de conexões esgotado em ambiente multi-tenant (SQL Server). Diagnóstico conduzido por eliminação de hipóteses, com procedimento de contenção validado em produção.

Fábio Medina Favarin · Documentação técnica · Caso real de produção.