Connection pooling mantém um conjunto de conexões reutilizáveis para banco de dados, Redis, serviços HTTP ou outros recursos caros. Em vez de abrir e fechar uma conexão para cada operação, a aplicação pega uma conexão do pool, executa o trabalho e devolve-a.
O pool reduz latência e overhead, mas também impõe um limite de concorrência. Quando todas as conexões estão ocupadas, novas operações aguardam. Dimensionamento errado pode esgotar o banco, aumentar filas ou causar timeouts em cascata.
Por que conexões são caras
A criação pode envolver:
- DNS;
- handshake TCP;
- TLS;
- autenticação;
- negociação de parâmetros;
- alocação de memória no servidor;
- inicialização de sessão.
Reutilização evita repetir esse custo.
Pool com PostgreSQL
import pg from 'pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
min: 2,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
});
const result = await pool.query(
'SELECT id, name FROM users WHERE id = $1',
[userId],
);pool.query() pega e devolve uma conexão automaticamente para uma única query.
Transações
Uma transação precisa usar o mesmo client:
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}Não use pool.query() para comandos de uma mesma transação, porque cada chamada pode receber conexão diferente.
Sempre liberar
Esquecer release() causa vazamento do pool. Use finally. Se a conexão ficou em estado inválido, descarte-a conforme a API do driver.
Dimensionamento
O total de conexões não é apenas o valor do pool:
conexões totais = réplicas × processos por réplica × max do poolDez pods com pool de 20 podem abrir 200 conexões. Some jobs, ferramentas administrativas e réplicas de leitura.
Limite do banco
O banco precisa reservar conexões para manutenção e operações internas. Não configure a soma dos pools igual ao limite máximo. Use margem e, quando necessário, um proxy como PgBouncer.
Little’s Law
Uma aproximação de concorrência é taxa multiplicada pela duração média. Se a aplicação executa 100 queries por segundo e cada query ocupa conexão por 50 ms, a concorrência média é cerca de cinco. Picos e p99 exigem margem.
Pool grande não resolve query lenta
Mais conexões podem aumentar contenção, locks, cache misses e CPU no banco. Primeiro otimize queries, índices e transações.
Fila de espera
Quando o pool está cheio, chamadas aguardam. Uma fila sem limite pode consumir todo o deadline. Aplique timeout para obter conexão e rejeite cedo quando necessário.
Tempo na fila
Meça separadamente:
- tempo aguardando conexão;
- tempo da query;
- tempo total.
Uma query rápida com fila longa indica pool saturado ou transações demoradas.
Connection timeout
O timeout de criação controla quanto esperar para abrir uma nova conexão. Ele não é necessariamente o timeout da query.
Statement timeout
Configure timeout de query no banco ou driver:
SET statement_timeout = '3s';Em produção, use configuração por sessão ou transação conforme o driver. Cancelar a Promise sem cancelar a query mantém o banco trabalhando.
Idle timeout
Conexões ociosas podem ser fechadas após um período. Valor baixo demais cria churn; alto demais consome slots. Considere tráfego, autoscaling e timeout de rede.
Conexões mínimas
Manter conexões aquecidas reduz latência após períodos ociosos, mas cada réplica consome recursos mesmo sem tráfego. Em ambientes serverless ou autoscaling agressivo, min zero pode ser melhor.
Health check do pool
Não execute uma query cara em toda liveness. Readiness pode verificar capacidade mínima ou uma consulta simples com cache curto. O uso real também fornece sinal.
Conexões quebradas
Rede, failover e restart do banco invalidam sockets. O pool deve remover conexões com erro e criar novas com backoff. Trate eventos do driver.
pool.on('error', (error) => {
logger.error({ error }, 'Erro em conexão ociosa do pool');
});Failover
Após troca do primário, conexões existentes podem apontar ao nó antigo. O pool precisa detectar falha e reconectar. DNS e proxies de banco podem ter TTL e comportamento próprios.
Prepared statements
Prepared statements podem ser associados à conexão. Um pool distribui queries entre sessões. Use nomes e cache de acordo com o driver, evitando criar milhares de statements únicos.
Estado de sessão
Alterações como timezone, search_path e variáveis persistem na conexão. Uma requisição pode devolver ao pool uma sessão modificada e afetar a próxima. Configure estado no checkout ou evite mudanças não restauradas.
Tenant por schema
Mudar schema por conexão exige reset no finally. Uma falha pode vazar contexto entre tenants. Prefira queries qualificadas ou transações com configuração local.
Transações longas
Uma transação mantém conexão ocupada e pode segurar locks. Não faça chamadas HTTP dentro dela. Prepare dados antes, execute a transação de forma curta e publique eventos depois por outbox.
N+1 queries
Um endpoint que executa centenas de queries pode monopolizar o pool. Use joins, batching, DataLoader ou consultas em lote.
Concorrência dentro da requisição
Promise.all com muitas queries não torna tudo mais rápido. Ele pode ocupar todo o pool:
// Evite para milhares de itens
await Promise.all(ids.map((id) => repository.findById(id)));
Use limite de concorrência ou uma query com WHERE id = ANY($1).
Pool por banco
Crie pools separados para destinos diferentes. Dentro do mesmo banco, múltiplos pools por módulo podem multiplicar conexões sem necessidade. Centralize o lifecycle.
Read e write pools
Réplicas de leitura podem usar pool separado. Considere consistência após escrita, lag e fallback para primário.
HTTP connection pooling
O mesmo conceito vale para agentes HTTP e Undici. Limite conexões por origem, consuma corpos e monitore fila.
Redis
Redis frequentemente usa poucas conexões multiplexadas. Criar um pool grande pode ser desnecessário. Consulte a biblioteca e o padrão de comandos bloqueantes ou Pub/Sub, que podem exigir conexões dedicadas.
Serverless
Muitas instâncias curtas podem criar tempestade de conexões. Use proxy gerenciado, limite pool por instância e reutilize o cliente entre invocações quando a plataforma permitir.
Kubernetes
Autoscaling baseado em CPU pode aumentar pods e conexões justamente durante pico. Inclua capacidade do banco no planejamento e limite máximo de réplicas.
Graceful shutdown
await stopAcceptingRequests();
await drainRequests();
await pool.end();Fechar o pool antes de drenar requisições causa falhas. Aplique timeout máximo.
Observabilidade
Monitore:
- conexões totais;
- ativas e ociosas;
- fila de espera;
- tempo de checkout;
- tempo de query;
- timeouts;
- erros de conexão;
- reconexões;
- transações abertas;
- queries por requisição.
Alertas
Alerte por saturação sustentada, fila crescente, timeout de checkout e conexões próximas ao limite do banco. Correlacione com latência e locks.
Teste de carga
Teste diferentes tamanhos de pool. Meça throughput, p99, CPU do banco, locks, tempo de fila e conexões. O melhor valor não é necessariamente o maior.
Erros comuns
- não liberar client;
- usar pool.query em transação;
- ignorar número de réplicas;
- pool maior que capacidade do banco;
- não limitar fila;
- fazer HTTP dentro de transação;
- usar Promise.all ilimitado;
- não resetar estado de sessão;
- fechar pool cedo no shutdown;
- aumentar pool para esconder query lenta.
Fluxo recomendado
Calcule o total global, comece com pool pequeno, meça fila e duração e ajuste com o banco. Libere conexões em finally e mantenha transações curtas. Combine com HTTP Keep-Alive, Undici, Graceful Shutdown e métricas em Prometheus.
Consulte a documentação de pooling do node-postgres e a documentação oficial do driver e banco utilizados no projeto.



