DevGuideTK
← Trilha Backend

Pool de conexões

Por que abrir uma conexão por pedido não escala e como dimensionar o pool.

O código desta página foi executado nas cinco linguagens contra um PostgreSQL 18. As medições com o pgbench e com o PgBouncer 1.26 estão no nível Conceito.

O pool configurado, em cada linguagem

Cada versão cria um pool com no máximo 5 conexões, espera de 1 segundo por uma conexão livre, fechamento das ociosas depois de 30 segundos e renovação a cada 30 minutos. Para mostrar a espera funcionando, o programa ocupa as 5 conexões com consultas de 3 segundos e faz uma sexta consulta.

import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 5,                         // conexões no máximo, por instância da aplicação
  connectionTimeoutMillis: 1000,  // quanto um pedido espera por uma conexão livre
  idleTimeoutMillis: 30_000,      // fecha conexões paradas há mais de 30 s
  maxLifetimeSeconds: 1800,       // renova cada conexão a cada 30 min
});

// pool.query pega uma conexão e a devolve sozinha, mesmo com erro
async function consultar(sql: string) {
  const inicio = Date.now();
  try {
    await pool.query(sql);
    return `ok em ${Date.now() - inicio} ms`;
  } catch (erro: any) {
    return `falhou em ${Date.now() - inicio} ms: ${erro.message}`;
  }
}

// Cinco consultas lentas ocupam o pool inteiro; a sexta não espera para sempre
const lentas = Array.from({ length: 5 }, () => consultar("SELECT pg_sleep(3)"));
await new Promise((r) => setTimeout(r, 200));
console.log("sexta, com o pool cheio:", await consultar("SELECT 1"));
console.log("lentas:", await Promise.all(lentas));
console.log("depois que liberaram:", await consultar("SELECT 1"));
await pool.end();

O resultado, nas cinco linguagens

Sexta consulta, com o pool cheioMensagemDepois que liberaram
TypeScript (pg)falhou em 1.002 mstimeout exceeded when trying to connectok em 3 ms
Python (psycopg_pool)falhou em 1.001 mscouldn't get a connection after 1.00 secok em 2 ms
Go (pgxpool)falhou em 1.000 mscontext deadline exceededok em menos de 1 ms
Java (HikariCP)falhou em 1.040 msConnection is not available, request timed out after 1001ms (total=5, active=5, idle=0, waiting=0)ok em 1 ms
C# (Npgsql)falhou em 1.006 msThe connection pool has been exhausted, either raise 'Max Pool Size' (currently 5) or 'Timeout' (currently 1 seconds)ok em 1 ms

Nas cinco, o pedido excedente falhou em cerca de 1 segundo, em vez de ficar esperando, e o pool voltou ao normal quando as consultas lentas terminaram. As mensagens do HikariCP e do Npgsql já dizem o estado do pool, o que ajuda muito num incidente.

O que muda de uma linguagem para outra

MáximoEspera por conexãoOciosaVida máxima
TypeScriptmaxconnectionTimeoutMillisidleTimeoutMillismaxLifetimeSeconds
Pythonmax_sizetimeoutmax_idlemax_lifetime
GoMaxConnsprazo do contextMaxConnIdleTimeMaxConnLifetime
JavamaximumPoolSizeconnectionTimeoutidleTimeoutmaxLifetime
C#Maximum Pool SizeTimeoutConnection Idle LifetimeConnection Lifetime

No Go, o pgxpool não tem uma espera própria: o prazo do context vale para conseguir a conexão e para a consulta. No C#, o pool do Npgsql é configurado na string de conexão, e o Timeout vale tanto para abrir uma conexão nova quanto para esperar uma livre. Em todas, o pool deve ser criado uma vez por processo e compartilhado, e não a cada pedido.

O vazamento, reproduzido

A rota abaixo pega uma conexão e, num caminho de retorno antecipado, esquece de devolvê-la:

async function rotaComVazamento() {
  const conexao = await pool.connect();
  const { rows } = await conexao.query("SELECT 1 AS n");
  if (rows[0].n === 1) return "ok"; // retorno antecipado: esqueceu o release()
  conexao.release();
}

Com um pool de 5 e espera de 1 segundo, os pedidos de 1 a 5 funcionaram, cada um prendendo uma conexão, e os pedidos 6 e 7 falharam com timeout exceeded when trying to connect. A correção é try { ... } finally { conexao.release(); }, ou usar pool.query, que pega e devolve a conexão sozinho. A métrica que denuncia o vazamento é o número de conexões emprestadas subindo sem voltar, mesmo sem carga.

O PgBouncer em modo transação

A configuração mínima, no arquivo pgbouncer.ini:

[databases]
loja = host=banco port=5432 dbname=loja

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 16
max_client_conn = 1000

O teste usou a imagem edoburu/pgbouncer, que gera esse arquivo a partir de variáveis de ambiente (POOL_MODE, DEFAULT_POOL_SIZE, MAX_CLIENT_CONN, AUTH_TYPE); o arquivo acima mostra as opções equivalentes e não foi usado como está. A aplicação conecta na porta do PgBouncer como se fosse o banco. Com pool_mode = transaction, troque todo SET por SET LOCAL dentro de uma transação: no teste, um SET de uma aplicação apareceu na sessão de outra.

Lista de revisão

Pool

  • Há um pool por processo, criado na inicialização, e não uma conexão por pedido?
  • O máximo por instância, somado em todas as instâncias e jobs, cabe no limite do banco?
  • O tamanho foi escolhido pelos núcleos do banco e medido com a carga real?
  • Há espera curta por conexão, para falhar rápido com o pool esgotado?
  • Há vida máxima e tempo ocioso configurados?

Uso

  • Toda conexão emprestada volta ao pool em todos os caminhos, inclusive com erro?
  • As transações são curtas, sem chamadas externas no meio?
  • Há métricas de conexões emprestadas, livres e pedidos esperando?

Intermediário

  • Com muitas instâncias ou funções sem servidor, há um intermediário na frente do banco?
  • Em modo transação, o código usa SET LOCAL e evita estado de sessão?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Configurar o pool na minha aplicação

    Quero configurar o pool de conexões da minha aplicação do jeito certo.
    
    Pergunte qual linguagem, framework, driver e banco eu uso.
    
    Vamos em etapas:
    1. Criar um pool por processo e compartilhá-lo.
    2. Configurar máximo, espera por conexão, tempo ocioso e vida máxima.
    3. Garantir que toda conexão volta ao pool, com as formas automáticas do meu driver.
    4. Expor métricas de conexões emprestadas, livres e pedidos esperando.
    5. Um teste que esgota o pool e confere que o pedido excedente falha rápido.
  • Encontrar a causa de um pool esgotado

    Minha aplicação está com erros de tempo esgotado ao pegar conexão do pool.
    
    Pergunte qual linguagem, driver e banco eu uso, peça a configuração do pool e o que as métricas mostram.
    
    Me guie na investigação:
    1. Distinguir carga real, consultas lentas e vazamento.
    2. As consultas ao banco para ver as conexões abertas e o que cada uma está fazendo (pg_stat_activity).
    3. Como achar no código onde a conexão não é devolvida.
    4. As correções para cada causa.
    5. O que monitorar para pegar o problema antes da próxima vez.
  • Revisar o uso de conexões

    Vou colar abaixo o código que cria e usa as conexões com o banco numa aplicação, e a configuração do pool. Revise para produção.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Há um pool por processo, ou conexões abertas por pedido?
    2. O tamanho do pool, somado nas instâncias, cabe no limite do banco?
    3. Há tempo limite para esperar uma conexão?
    4. Toda conexão emprestada volta em todos os caminhos?
    5. Há vida máxima e tempo ocioso?
    6. Atrás de um PgBouncer em modo transação, o código usa estado de sessão?
    
    Para cada problema, descreva como ele aparece em produção e mostre a correção.
    
    Código e configuração:
    [cole aqui]