Blog

Limite de conexões do banco esgotado: quando o pool vira o gargalo do serviço

O erro chegou às onze da manhã de uma terça-feira comum, dizendo que o banco recusou a conexão porque atingiu o limite de clientes, e a primeira reação do time foi aumentar o número máximo de conexões no servidor. Funcionou por quarenta minutos. Depois o banco voltou a recusar, agora com a CPU em noventa por cento e consultas que antes levavam dois milissegundos levando quatrocentos. Este artigo mostra por que aumentar o limite é a única resposta que piora o problema de forma garantida, qual é a conta que dimensiona o pool a partir da capacidade real do banco e não do número de contêineres, por que a fila de espera do pool é a métrica que enxerga o problema antes do erro aparecer, como uma transação que espera resposta de HTTP consome uma conexão sem usar o banco, qual é a diferença prática entre pool por processo e pool centralizado e quando cada um vale a pena, e quais três alertas mostram o esgotamento com antecedência suficiente para agir.

2026-09-09 / Arquitetura / 18 min

01

A conexão do banco é um recurso caro, não um objeto barato

A camada de acesso a dados da maior parte das aplicações apresenta a conexão como se ela fosse um detalhe de implementação, e o resultado é que quase ninguém sabe quanto ela custa. Numa base relacional que usa processo por conexão, como o PostgreSQL, cada conexão aberta corresponde a um processo do sistema operacional com sua própria área de memória de trabalho, seu próprio cache de catálogo e sua própria participação nas estruturas compartilhadas que o banco precisa varrer a cada snapshot de transação. O número que aparece no arquivo de configuração como limite máximo não é uma trava arbitrária que alguém colocou por precaução: ele é a capacidade que o banco consegue sustentar antes que o custo de coordenar processos passe a consumir mais do que o trabalho útil.

Isso explica o comportamento que confunde quem investiga o incidente pela primeira vez. Enquanto o número de conexões ativas está abaixo do ponto de saturação, adicionar concorrência aumenta a vazão de forma quase linear, e a intuição de que mais conexões significa mais capacidade se confirma. Depois desse ponto, a vazão para de crescer e a latência começa a subir, porque as consultas passam a competir por CPU, por páginas do cache compartilhado e por trilhas de disco. Continuando a adicionar, a vazão cai de verdade, e o sistema entra na região em que mais concorrência produz menos trabalho concluído por segundo. O erro de conexão recusada aparece perto dessa região e é interpretado como falta de conexões, quando na prática ele é o banco recusando entrar num regime onde ninguém sairia ganhando.

VAZAO x CONEXOES ATIVAS (forma tipica)

  vazao
   ^
   |            .--------.
   |          .'          '-.
   |        .'                '--.
   |      .'                       '---.
   |    .'                               '----.
   |  .'                                        '-----
   +--+-----------+------------+----------------------> conexoes ativas
      A           B            C

  A = subutilizado: adicionar conexao aumenta vazao
  B = ponto de saturacao: perto de (nucleos x 2) + discos efetivos
  C = colapso: mais conexao, menos trabalho concluido por segundo

  O erro "too many clients" aparece em C.
  Aumentar o limite move o sistema para a DIREITA de C.

Daí vem a conclusão que orienta todo o resto: o limite de conexões não é um teto de segurança que atrapalha, é o mecanismo que impede o banco de entrar em colapso por excesso de concorrência. Aumentar o limite remove a proteção sem adicionar capacidade nenhuma, e transforma um erro rápido e visível, que a aplicação pode tratar, num degrau de latência distribuído por todas as requisições, que ninguém consegue atribuir a nada. O erro de conexão recusada é um sintoma incômodo mas honesto. A alternativa produzida por aumentar o limite é um sistema lento sem culpado aparente.

02

A conta que dimensiona o pool a partir da capacidade do banco

O dimensionamento do pool costuma ser feito de trás para frente. Alguém pega o número de requisições simultâneas que a aplicação recebe no pico, arredonda para cima e usa isso como tamanho do pool, porque parece razoável que cada requisição tenha a sua conexão. O problema é que esse raciocínio parte da demanda e ignora a oferta. O tamanho correto do pool não vem do número de requisições que chegam, vem da quantidade de trabalho simultâneo que o banco consegue executar sem degradar, e esse número é surpreendentemente pequeno.

A regra de partida mais usada estima a concorrência útil como o dobro do número de núcleos de CPU do servidor de banco somado ao número de discos que servem leituras efetivas em paralelo. O dobro dos núcleos existe porque metade do tempo de uma consulta típica é gasto esperando entrada e saída, então cada núcleo consegue intercalar duas consultas sem ficar ocioso. Em máquina com armazenamento em memória flash e conjunto de dados que cabe no cache compartilhado, a parcela de disco tende a zero e o número converge para o dobro dos núcleos. Um servidor com oito núcleos, portanto, sustenta algo próximo de dezesseis a vinte conexões executando trabalho ao mesmo tempo, e não as duzentas que a soma dos pools costuma produzir.

// Dimensionamento do pool a partir da capacidade do banco,
// e nao do numero de requisicoes simultaneas da aplicacao.

// 1) Capacidade util do banco (concorrencia que ele sustenta sem degradar).
const NUCLEOS_BANCO = 8;
const DISCOS_EFETIVOS = 0;          // 0 em NVMe com dataset em cache
const CONEXOES_RESERVADAS = 5;      // superusuario, replicacao, migracao, backup

const capacidadeUtil = NUCLEOS_BANCO * 2 + DISCOS_EFETIVOS;   // 16

// 2) Divisao entre os consumidores. Toda instancia que abre pool conta,
//    inclusive worker, cron e job de relatorio, que sempre sao esquecidos.
const consumidores = [
  { nome: 'api', instancias: 6, peso: 3 },        // trafego sincrono
  { nome: 'worker-fila', instancias: 4, peso: 2 }, // processamento assincrono
  { nome: 'cron-relatorio', instancias: 1, peso: 1 },
];

const pesoTotal = consumidores.reduce(
  (soma, c) => soma + c.instancias * c.peso,
  0,
);                                                  // 6*3 + 4*2 + 1*1 = 27

const orcamento = capacidadeUtil - CONEXOES_RESERVADAS; // 11 conexoes uteis

const plano = consumidores.map((c) => {
  const fatia = (c.instancias * c.peso) / pesoTotal;
  const total = Math.max(1, Math.floor(orcamento * fatia));
  return {
    servico: c.nome,
    poolPorInstancia: Math.max(1, Math.floor(total / c.instancias)),
    totalDoServico: total,
  };
});

console.table(plano);
// api            -> pool 1 por instancia, 7 no total
// worker-fila    -> pool 1 por instancia, 3 no total
// cron-relatorio -> pool 1, 1 no total
//
// Total conectado ao banco: 11. Parece pouco e provoca reacao imediata:
// "com pool 1 a api nao aguenta 400 requisicoes por segundo". Aguenta,
// desde que a consulta dure 5 ms: 1 conexao x (1000 / 5) = 200 req/s por
// instancia, 1200 req/s no conjunto. O limite nunca foi a conexao,
// sempre foi o tempo que cada uma fica ocupada.

// 3) Verificacao: a vazao teorica precisa cobrir o pico com folga.
const DURACAO_MEDIA_MS = 5;
const vazaoTeorica = plano
  .filter((p) => p.servico === 'api')
  .reduce((soma, p) => soma + p.totalDoServico * (1000 / DURACAO_MEDIA_MS), 0);

console.log(`vazao teorica da api: ${vazaoTeorica} req/s`);  // 1400 req/s

A verificação no final do exemplo é a parte que muda a conversa dentro do time. Um pool pequeno provoca resistência imediata porque o número parece incompatível com o volume de tráfego, e a objeção só se dissolve quando a vazão é calculada explicitamente. Uma conexão que executa consultas de cinco milissegundos entrega duzentas consultas por segundo. Se a mesma consulta passa a levar cinquenta milissegundos, ela entrega vinte, e nenhum tamanho de pool compensa isso, porque a fila apenas transfere a espera para dentro da aplicação. É por isso que otimizar a consulta lenta libera mais capacidade do que qualquer ajuste de pool, e é por isso que o dimensionamento precisa vir depois da medição de duração, nunca antes.

03

A fila de espera do pool é a métrica que vê o problema chegando

A instrumentação padrão de um pool de conexões costuma expor o número de conexões ativas e o número de conexões ociosas, e esses dois valores dizem muito pouco sobre saúde. Conexões ativas em número alto podem significar tanto um sistema saudável trabalhando no limite planejado quanto um sistema afogado. O que separa os dois casos é uma terceira métrica, quase sempre disponível e quase nunca observada: o tempo que uma requisição passa esperando para receber uma conexão do pool.

Essa métrica tem uma propriedade que a torna especialmente útil. Ela é zero enquanto existe folga e sobe de forma abrupta quando a folga acaba, o que significa que ela não avisa cedo demais nem tarde demais. Diferente da taxa de erro, que só se move quando o tempo limite de aquisição já foi ultrapassado e a requisição já falhou, o tempo de espera começa a subir no momento em que a demanda encosta na capacidade, e costuma dar de um a cinco minutos de antecedência num pico de crescimento típico. Diferente do número de conexões ativas, ela não depende do tamanho do pool para ser interpretada: espera acima de zero significa saturação, em qualquer configuração.

MétricaO que ela respondeQuando se moveLimite prático
Conexões ativasQuantas conexões estão executando consulta agoraJunto com o tráfegoNão distingue saudável de afogado
Conexões ociosasQuanta folga instantânea existe no poolJunto com o tráfegoCai a zero antes de o problema aparecer, sem avisar quanto falta
Tempo de espera por conexão (p95)Quanto a requisição espera antes de trabalharNo instante em que a demanda encosta na capacidadePrecisa de instrumentação explícita na maioria dos pools
Duração da conexão em uso (p95)Por quanto tempo cada conexão fica retidaQuando entra consulta lenta ou chamada externa na transaçãoSobe também por causas fora do banco, o que é justamente o valor
Timeout de aquisição por minutoQuantas requisições desistiram de esperarDepois que o usuário já viu o erroServe para confirmar, nunca para prevenir

A quarta linha da tabela merece atenção porque ela é a que explica a maior parte dos esgotamentos que não têm relação com aumento de tráfego. A duração da conexão em uso sobe quando a conexão fica retida por algo que não é o banco, e quando isso acontece o pool esgota com o mesmo volume de sempre. A causa mais comum é uma chamada de rede dentro de um bloco que já abriu a transação, e é o assunto da próxima seção.

// Instrumentacao minima do tempo de espera do pool.
// A ideia vale para qualquer driver: medir o intervalo entre pedir a
// conexao e receber, separado do tempo de execucao da consulta.

import { Pool } from 'pg';
import { performance } from 'node:perf_hooks';

const pool = new Pool({
  max: 7,                          // vindo do dimensionamento, nao do palpite
  connectionTimeoutMillis: 2000,   // desistir rapido, nao esperar sem limite
  idleTimeoutMillis: 30000,
});

export async function comConexao(rotulo, executar) {
  const pedidoEm = performance.now();
  let cliente;

  try {
    cliente = await pool.connect();
  } catch (erro) {
    metricas.incrementar('db.pool.timeout_aquisicao', { rotulo });
    throw erro;
  }

  const esperaMs = performance.now() - pedidoEm;
  metricas.histograma('db.pool.espera_ms', esperaMs, { rotulo });

  const usoEm = performance.now();
  try {
    return await executar(cliente);
  } finally {
    metricas.histograma('db.pool.uso_ms', performance.now() - usoEm, { rotulo });
    cliente.release();
  }
}

// O rotulo por caminho de codigo e o que torna a metrica acionavel:
// sem ele o painel mostra que o pool saturou, com ele mostra que quem
// segura a conexao por 800 ms e o relatorio, nao o checkout.

04

Transação que espera resposta de rede consome conexão sem usar o banco

Existe uma categoria de esgotamento que não aparece em nenhum gráfico de tráfego porque o tráfego não mudou. O pool esgota, o banco continua com CPU baixa, as consultas continuam rápidas quando executadas manualmente, e a soma disso deixa o time procurando no lugar errado. A causa é quase sempre a mesma: alguma transação passou a segurar a conexão enquanto espera por algo que não é o banco.

O padrão nasce de uma intenção correta. O desenvolvedor quer que a criação do pedido e o registro da cobrança sejam atômicos, então envolve os dois numa transação. Como a cobrança acontece num provedor externo, a chamada de rede acaba dentro do bloco transacional. Enquanto o provedor responde em cento e cinquenta milissegundos, ninguém percebe. No dia em que o provedor degrada para três segundos, cada pedido passa a reter uma conexão por três segundos, e um pool que atendia tranquilamente cem pedidos por minuto satura com dez. O banco não tem culpa nenhuma e o painel do banco não mostra nada.

// ERRADO: a chamada HTTP acontece dentro da transacao.
// A conexao fica retida pelo tempo de resposta do provedor externo.
async function criarPedidoErrado(dados) {
  return db.transaction(async (tx) => {
    const pedido = await tx.pedidos.insert(dados);

    // Aqui a conexao esta aberta, com transacao ativa, segurando locks,
    // esperando um servico que voce nao controla.
    const cobranca = await gatewayPagamento.cobrar({
      pedidoId: pedido.id,
      valor: pedido.total,
    });

    await tx.cobrancas.insert({ pedidoId: pedido.id, externoId: cobranca.id });
    return pedido;
  });
}

// CERTO: a transacao cobre apenas o trabalho de banco.
// A chamada externa fica fora, e a atomicidade vira uma maquina de estados.
async function criarPedido(dados) {
  // Transacao 1: curta, so escreve estado local e registra a intencao.
  const pedido = await db.transaction(async (tx) => {
    const criado = await tx.pedidos.insert({ ...dados, status: 'aguardando_cobranca' });
    await tx.outbox.insert({
      tipo: 'cobranca.solicitar',
      pedidoId: criado.id,
      chaveIdempotencia: `pedido-${criado.id}`,
    });
    return criado;
  });

  return pedido;
}

// O worker do outbox faz a chamada externa SEM conexao de banco retida,
// e so pega uma conexao de volta para gravar o resultado.
async function processarCobranca(evento) {
  const cobranca = await gatewayPagamento.cobrar({
    pedidoId: evento.pedidoId,
    chaveIdempotencia: evento.chaveIdempotencia,  // seguro para retentativa
  });

  await db.transaction(async (tx) => {           // transacao 2: curta de novo
    await tx.cobrancas.insert({
      pedidoId: evento.pedidoId,
      externoId: cobranca.id,
    });
    await tx.pedidos.update(evento.pedidoId, { status: 'cobrado' });
  });
}

A troca que esse desenho faz precisa ser dita com clareza para não parecer gratuita. A versão errada oferece atomicidade real entre as duas escritas, e a versão correta não oferece: existe um intervalo em que o pedido está criado e a cobrança ainda não aconteceu. O que se ganha em troca é que a duração da transação deixa de depender de um sistema externo, o que significa que a degradação do provedor vira atraso na fila em vez de esgotamento do pool. Além disso, a atomicidade da versão errada era parcialmente ilusória: se a aplicação caísse depois da cobrança e antes do commit, a transação faria rollback e o cliente estaria cobrado sem pedido. A chave de idempotência no worker resolve isso de forma explícita, o que a transação nunca resolveu.

  • Chamada HTTP a serviço externo dentro do bloco transacional, o caso mais frequente e o mais caro.
  • Escrita em fila ou tópico de mensageria antes do commit, que adiciona a latência do broker à duração da transação.
  • Laço que processa uma lista item a item com uma consulta por item, mantendo a conexão retida durante todo o percurso.
  • Leitura de arquivo, geração de PDF ou processamento de imagem no meio da transação, retendo conexão durante trabalho de CPU.
  • Espera por bloqueio de outra transação, que não aparece como consulta lenta porque a consulta ainda nem começou.
  • Sessão de depuração ou console interativo aberto contra o banco de produção, que sozinho consome uma conexão por horas.

05

Pool por processo contra pool centralizado

O pool por processo é o arranjo padrão e funciona bem enquanto o número de processos é estável e conhecido. Ele deixa de funcionar no momento em que a escala automática entra em cena, porque o total de conexões abertas passa a ser o tamanho do pool multiplicado por um número que muda sozinho. Um pool de dez conexões em seis instâncias consome sessenta conexões, o que é administrável. O mesmo pool com a escala configurada para quarenta instâncias no pico consome quatrocentas, e o limite do banco é atingido pela política de escala e não pelo tráfego.

Existe ainda um agravante que passa despercebido em ambiente sem servidor dedicado, onde cada invocação pode criar seu próprio pool. Nesse arranjo, o número de conexões acompanha a concorrência de invocações, e a ideia de dimensionar o pool perde sentido porque não existe um processo de vida longa para segurá-lo. É o cenário em que o pool centralizado deixa de ser uma otimização e passa a ser requisito de funcionamento.

AspectoPool por processoPool centralizado
Total de conexões no bancoTamanho do pool multiplicado por instâncias, cresce com a escalaFixo e configurado num ponto só, independente da escala
Comportamento na escala automáticaCada instância nova abre conexões, o pico de escala vira pico de conexãoInstância nova conecta ao intermediário, o banco não percebe
Latência adicional por consultaNenhumaUm salto de rede a mais, tipicamente abaixo de um milissegundo na mesma rede
Transação e recurso de sessãoSuporte total: transação longa, prepared statement, tabela temporáriaDepende do modo: no modo por transação, recurso de sessão quebra
Ponto único de falhaFalha isolada por instânciaO intermediário precisa de redundância própria
Quando compensaNúmero de instâncias estável e conhecidoEscala automática, ambiente sem servidor, muitos serviços no mesmo banco

A linha sobre modo de operação é a que produz incidente quando ignorada. No modo por transação, o intermediário devolve a conexão física ao conjunto assim que a transação termina, o que é justamente o que permite atender muitos clientes com poucas conexões. A consequência é que qualquer estado preso à sessão deixa de valer entre uma consulta e a seguinte: prepared statements nomeados, tabelas temporárias, variáveis de sessão e bloqueios consultivos. A migração para esse modo é simples do lado da configuração e exige revisão do lado da aplicação, e o erro clássico é fazer a primeira parte e descobrir a segunda em produção, com o driver reclamando de um prepared statement que não existe mais.

06

Sequência de diagnóstico e os três alertas que dão antecedência

Quando o erro de conexão recusada aparece, a sequência abaixo separa em poucos minutos os três cenários possíveis, que exigem respostas completamente diferentes: demanda cresceu de verdade, alguma coisa está segurando conexão por tempo demais, ou existem conexões abandonadas que ninguém está usando. Fazer o diagnóstico nessa ordem evita a resposta reflexa de aumentar o limite.

  1. Liste as conexões por estado e por aplicação de origem. Se a maioria estiver em estado ocioso dentro de transação, o problema é transação aberta sem trabalho e não falta de capacidade.
  2. Meça o tempo da conexão mais antiga em cada estado. Uma conexão ociosa dentro de transação há mais de trinta segundos indica código que abriu transação e foi esperar por rede, por bloqueio ou por entrada humana.
  3. Compare o total de conexões com a soma teórica dos pools configurados. Se o total for maior, existe algum consumidor não inventariado: migração, ferramenta de análise, console aberto ou serviço legado.
  4. Verifique o tempo de espera do pool na aplicação, e não só no banco. Espera alta com poucas conexões ativas significa que o pool da aplicação está subdimensionado em relação ao que o banco aceitaria.
  5. Meça a duração das consultas no percentil noventa e cinco na última hora e compare com a semana anterior. Consulta que dobrou de duração dobra a demanda de conexões sem que nenhum usuário a mais tenha chegado.
  6. Só depois de excluir os quatro anteriores, avalie se a capacidade do banco é realmente o limite, e nesse caso a decisão é ampliar a máquina ou distribuir leitura para réplica, não aumentar o limite na mesma máquina.
-- Passos 1 a 3 do diagnostico, em PostgreSQL.

-- 1) Distribuicao por estado e por aplicacao de origem.
SELECT
  application_name,
  state,
  count(*) AS conexoes,
  max(now() - state_change) AS mais_antiga
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY application_name, state
ORDER BY conexoes DESC;

-- 2) Transacoes abertas sem trabalho ativo: o padrao mais caro.
--    Cada linha aqui e uma conexao retida sem usar o banco.
SELECT
  pid,
  application_name,
  now() - xact_start   AS transacao_aberta_ha,
  now() - state_change AS parada_ha,
  left(query, 120)     AS ultima_consulta
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND now() - state_change > interval '30 seconds'
ORDER BY parada_ha DESC;

-- 3) Consumo real contra o limite configurado.
SELECT
  (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend')
    AS conexoes_em_uso,
  current_setting('max_connections')::int
    AS limite,
  current_setting('superuser_reserved_connections')::int
    AS reservadas;

-- Rede de seguranca no servidor, para que codigo esquecido nao
-- consuma conexao indefinidamente. Vale por banco ou por papel.
ALTER DATABASE aplicacao SET idle_in_transaction_session_timeout = '15s';
ALTER ROLE relatorios  SET statement_timeout = '30s';

O tempo limite de sessão ociosa dentro de transação no final do exemplo é a única configuração do servidor que vale ajustar antes de qualquer mudança de limite. Ele transforma um vazamento silencioso, que consome conexão até alguém reiniciar o serviço, num erro imediato e atribuível ao caminho de código responsável. É uma configuração que gera reclamação no primeiro dia e evita incidente para sempre, porque o erro aparece no ambiente de testes com o mesmo comportamento que teria em produção.

  • Tempo de espera do pool no percentil noventa e cinco acima de cinquenta milissegundos por cinco minutos seguidos: a demanda encostou na capacidade e o esgotamento vem em seguida.
  • Conexões em estado ocioso dentro de transação acima de dois por cento do total por três minutos: existe código segurando transação sem trabalhar, e o volume atual apenas ainda não expôs o problema.
  • Razão entre conexões em uso e limite acima de setenta por cento no percentil noventa e cinco da janela de uma hora: a folga acabou e o próximo evento de escala provoca recusa, mesmo sem crescimento de tráfego.

FAQ

Perguntas frequentes

Por que um pool pequeno entrega mais vazão que um pool grande se as requisições vão ficar na fila?

Porque a fila do pool é mais barata que a fila do banco, e as duas existem de qualquer jeito. Quando o pool é grande o suficiente para que todas as requisições recebam conexão imediatamente, elas não deixam de esperar: elas passam a esperar dentro do banco, competindo por CPU, por páginas do cache compartilhado e por bloqueios, e essa espera é destrutiva porque o próprio ato de coordenar mais processos consome recursos que deixam de executar consulta. Com o pool pequeno, a espera acontece antes de a requisição tocar o banco, o banco trabalha na concorrência em que é mais eficiente, e cada consulta termina no menor tempo possível, o que faz a conexão ficar disponível mais cedo para a próxima da fila. O efeito prático é que a vazão total sobe e a latência no percentil noventa e cinco cai ao mesmo tempo, o que parece contraditório mas é apenas a diferença entre uma fila ordenada e uma disputa. A analogia que costuma convencer o time é a do caixa de supermercado: dez caixas abertos com um operador cada atendem mais gente por hora do que trinta caixas abertos com o mesmo operador correndo entre eles. Existe um limite inferior, claro, e ele é dado pela vazão teórica: se a duração média da consulta multiplicada pelo pico de requisições por segundo exceder o tamanho do pool, a fila cresce sem parar e a resposta correta é reduzir a duração da consulta, não ampliar o pool.

Quando faz sentido separar pools por tipo de carga em vez de usar um pool único por aplicação?

Faz sentido no momento em que uma carga lenta e tolerante a atraso divide o mesmo pool com uma carga rápida e sensível a latência, porque nesse arranjo a lenta sempre vence a disputa por acidente. Um relatório que retém a conexão por oitocentos milissegundos ocupa o mesmo espaço que cento e sessenta consultas de checkout de cinco milissegundos, e como o pool não distingue as duas, uma sequência de relatórios simultâneos faz o checkout falhar. A separação em pools independentes, cada um com seu próprio limite, cria um isolamento que impede que uma carga consuma a capacidade da outra: o relatório passa a esperar na sua própria fila e o checkout mantém as conexões que lhe foram reservadas. A divisão que costuma funcionar tem três pools, um para tráfego síncrono de usuário com o maior orçamento e tempo limite de aquisição curto, um para processamento assíncrono com orçamento médio e tempo limite generoso, e um pequeno para trabalho analítico com orçamento mínimo e tempo limite de instrução agressivo. Vale acrescentar que o pool analítico é o candidato natural a apontar para uma réplica de leitura em vez do primário, o que remove a carga do banco principal em vez de apenas isolá-la. O custo da separação é que a soma dos limites precisa continuar respeitando a capacidade total do banco, então dividir pools sem revisar a conta apenas redistribui o esgotamento.

Como identificar qual caminho de código está segurando conexões por tempo demais quando nada mudou no tráfego?

O primeiro passo é garantir que cada conexão carregue a identificação de quem a abriu, porque sem isso o diagnóstico depende de adivinhação. A maioria dos drivers permite definir o nome da aplicação na conexão, e vale usar um valor composto pelo serviço e pela versão implantada, o que faz a origem aparecer diretamente nas visões de atividade do banco. Um passo além, mais barato do que parece, é anexar um comentário estruturado à consulta com o caminho de código, o identificador do rastro distribuído e o nome do trabalho, porque esse comentário viaja junto com o texto da consulta e aparece nas visões de estatística e nos registros de consulta lenta, o que permite atribuir uma conexão retida a uma rota específica sem instrumentação adicional. Com isso disponível, a consulta que lista sessões ociosas dentro de transação passa a responder diretamente qual rota é a responsável, em vez de mostrar apenas um identificador de processo anônimo. Do lado da aplicação, a métrica que fecha o diagnóstico é o histograma da duração da conexão em uso rotulado por caminho de código, porque ela expõe o percentil noventa e nove por rota e revela o caso raro que retém por segundos enquanto a mediana permanece em milissegundos. Quando nada mudou no tráfego e o pool passou a esgotar, a resposta quase sempre aparece nessa cauda: uma rota pouco usada que começou a esperar por uma dependência externa lenta dentro de uma transação, ou um laço que passou a percorrer uma lista que cresceu.

O limite de conexões protege o banco, e o pool traduz capacidade em política

O erro de conexão recusada raramente significa falta de conexões: ele significa que a demanda de concorrência ultrapassou o que o banco sustenta sem degradar, e aumentar o limite apenas troca um erro visível por lentidão sem culpado. Dimensionar o pool a partir da capacidade real, medir o tempo de espera antes que o erro apareça e tirar chamada externa de dentro da transação resolvem a maior parte dos casos sem tocar em infraestrutura. Posso dimensionar os pools do seu sistema a partir da capacidade do banco, instrumentar o tempo de espera e a duração da conexão em uso por caminho de código, revisar as transações que retêm conexão esperando rede, avaliar se o pool centralizado compensa no seu arranjo de escala e configurar os três alertas que dão antecedência real.