Knowledge

Connection pooling em 2026: estratégias para bancos de dados em sistemas de alta escala

O gerenciamento correto de connection pooling é a diferença entre um sistema que escala suavemente e um que entra em colapso sob carga.

18/03/20265 min de leituraKnowledge
Connection pooling em 2026: estratégias para bancos de dados em sistemas de alta escala

Resumo executivo

O gerenciamento correto de connection pooling é a diferença entre um sistema que escala suavemente e um que entra em colapso sob carga.

Ultima atualizacao: 18/03/2026

Resumo executivo

Connection pooling é uma das peças mais críticas e frequentemente mal-configuradas na arquitetura de sistemas de alta escala. Em 2026, com aplicações serverless, microserviços e workloads de alta concorrência, a configuração incorreta do pool de conexões continua sendo uma das causas mais comuns de degradação de performance e timeouts de banco de dados.

O problema é sistêmico: muitos times assumem que "mais conexões é melhor" ou que "o default do pool está bom", resultando em resource contention, connection storms e eventual colapso do banco de dados. PostgreSQL, por exemplo, tem um limite prático de ~1000 conexões simultâneas por instância — além disso, performance degrada exponencialmente.

Para arquitetos e engenheiros, a configuração de connection pooling é um exercício de balanceamento: conexões suficientes para paralelizar workload, mas não tantas que sobrecarreguem o banco de dados ou a aplicação.

O problema fundamental: custo de conexões

Cada conexão de banco de dados não é "barata":

Custo no servidor (PostgreSQL):

  • RAM: ~2-10MB por conexão (depende de work_mem, shared_buffers)
  • CPU: overhead de context switching entre conexões
  • Limites: max_connections (default 100, mas 500-1000 é o prático)

Custo no cliente (aplicação):

  • RAM: Socket buffers, state tracking
  • CPU: Multiplexing de I/O entre múltiplas conexões
  • Latência: Round-trip de estabelecimento de conexão

Custo de transmissão:

  • TCP handshake: ~1-2 RTTs (local), ~10-50ms (cross-region)
  • TLS handshake: adicional ~1-2 RTTs se SSL habilitado
  • Authentication: query de autenticação após conexão estabelecida

Impacto real: Em uma aplicação com 10.000 requisições/segundo, abrir uma nova conexão por requisição adiciona 10-50ms de latência e degrada performance do banco em 50-100%.

Arquitetura de pooling: layer de aplicação vs. proxy

Layer de aplicação (client-side pool):

Bibliotecas como pg-pool (Node.js), SQLAlchemy (Python), HikariCP (Java):

[App Instance 1]    [App Instance 2]    [App Instance 3]
       ↓                   ↓                   ↓
 [Connection Pool]  [Connection Pool]  [Connection Pool]
       ↓                   ↓                   ↓
       └───────────→ [PostgreSQL] ←────────────┘
                    (Direct connections)

Vantagens:

  • Latência zero adicional (sem hop extra)
  • Configuração por aplicação (flexibilidade)
  • Simples de implementar

Desvantagens:

  • Não centraliza conexões (multiplicação de instâncias = multiplicação de conexões)
  • Serverless/Functions: connections per execution (pool ineficiente)
  • Load balancing manual entre instâncias

Proxy (server-side pool):

PgBouncer, Pgpool-II, proxies de nuvem:

[App Instance 1]    [App Instance 2]    [App Instance 3]
       ↓                   ↓                   ↓
       └───────────→ [PgBouncer] ←────────────┘
                         ↓
                    [PostgreSQL]
                    (Fewer connections)

Vantagens:

  • Centralização de conexões (múltiplas apps compartilham pool)
  • Eficiente para serverless/functions
  • Connection limiting natural
  • Reuso de conexões entre instâncias

Desvantagens:

  • Latência adicional (~1-5ms por query)
  • Single point of failure (requer HA)
  • Complexidade operacional adicional

Configuração de pool: determinando tamanho ideal

A fórmula de Little (adaptada para connection pooling):

pool_size = (number_of_cores × target_utilization) × (execution_time + wait_time) / execution_time

Regra prática simplificada:

pool_size = number_of_cores × 2

Exemplo:

  • Servidor de aplicação: 16 cores
  • Carga: CPU-bound queries (10ms execução)
  • Target utilization: 70%
  • Pool size: 16 × 2 = 32 conexões

A abordagem de camadas:

┌─────────────────────────────────────┐
│ Application Server (16 cores)       │
│  ┌───────────────────────────────┐ │
│  │ Connection Pool: 32 connections │ │
│  └───────────────────────────────┘ │
└─────────────────────────────────────┘
                 ↓
         ┌───────────────┐
         │  PgBouncer    │
         │ Pool: 100 con │
         └───────────────┘
                 ↓
         ┌───────────────┐
         │  PostgreSQL   │
         │ max_conn: 200 │
         └───────────────┘

Configurações típicas por escala:

Instâncias de AppCores por InstânciaPool por InstânciaPool PgBouncerPostgreSQL max_conn
Pequeno (<10)2-410-2050-100100-150
Médio (10-50)4-820-40100-200200-300
Grande (50-200)8-1640-80200-500400-600
Muito Grande (>200)16+80-160500-1000800-1200

PgBouncer: configuração de produção

Modos de operação PgBouncer:

  1. Session pooling: Reutiliza conexões por sessão (transação)
  • Vantagem: Simples, compatível com todas as aplicações
  • Desvantagem: Menos eficiente em alta concorrência
  1. Transaction pooling: Reutiliza conexões por transação
  • Vantagem: Mais eficiente, escala melhor
  • Desvantagem: Incompatível com session-bound features (prepared statements, advisory locks)
  1. Statement pooling: Reutiliza conexões por statement
  • Vantagem: Mais eficiente para workloads de alta frequência
  • Desvantagem: Menos compatível com aplicações legadas

Configuração típica (pgbouncer.ini):

ini[databases]
primary = host=db-primary.internal port=5432
replica = host=db-replica.internal port=5432

[pgbouncer]
pool_mode = transaction
listen_addr = 0.0.0.0
listen_port = 6432
max_client_conn = 1000
default_pool_size = 100
reserve_pool_size = 10
reserve_pool_timeout = 3
max_db_connections = 500
server_lifetime = 3600
server_idle_timeout = 600
query_timeout = 60

# Logging
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1

Tuning por workload:

ini# Workload transaction-heavy (APIs, OLTP)
pool_mode = transaction
default_pool_size = 100
reserve_pool_size = 20

# Workload analytic-heavy (queries longas)
pool_mode = session
default_pool_size = 50
reserve_pool_size = 10

# Workload serverless (muitas conexões, queries curtas)
pool_mode = transaction
default_pool_size = 200
reserve_pool_size = 50

Monitoramento de connection pooling

Métricas críticas:

  1. Pool utilization:
  • Conexões ativas vs. total do pool
  • Esperas por conexão disponível
  • Rejections por pool exaustão
  1. Database connections:
  • Conexões ativas no PostgreSQL
  • Idle connections
  • Connection churn (rate de abertura/fechamento)
  1. Latência:
  • Latência de checkout do pool (tempo para obter conexão)
  • Latência de query (excluindo tempo de pool)
  • P50, P95, P99 latências

Queries de monitoramento PostgreSQL:

sql-- Conexões ativas por database
SELECT
  datname,
  count(*) as active_connections,
  count(*) FILTER (WHERE state = 'active') as active_queries,
  count(*) FILTER (WHERE state = 'idle') as idle_connections
FROM pg_stat_activity
WHERE datname IS NOT NULL
GROUP BY datname;

-- Conexões long-running (>5 minutos)
SELECT
  pid,
  usename,
  datname,
  state,
  query_start,
  now() - query_start as duration,
  query
FROM pg_stat_activity
WHERE state = 'active'
  AND now() - query_start > interval '5 minutes';

-- Queries bloqueadas
SELECT
  blocked_locks.pid AS blocked_pid,
  blocked_activity.usename AS blocked_user,
  blocking_locks.pid AS blocking_pid,
  blocking_activity.usename AS blocking_user,
  blocked_activity.query AS blocked_statement,
  blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
  ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
  ON blocking_locks.locktype = blocked_locks.locktype
  AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
  AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
  AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
  AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
  AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
  AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
  AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
  AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
  AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
  AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity
  ON blocking_activity.pid = blocking_locks.pid;

Métricas PgBouncer:

bash# Estatísticas do pool
echo "SHOW STATS;" | psql -h localhost -p 6432 pgbouncer

# Conexões ativas
echo "SHOW CLIENTS;" | psql -h localhost -p 6432 pgbouncer

# Lista de databases
echo "SHOW DATABASES;" | psql -h localhost -p 6432 pgbouncer

# Configuração atual
echo "SHOW CONFIG;" | psql -h localhost -p 6432 pgbouncer

Padrões avançados: connection pooling em arquiteturas modernas

Serverless / Functions:

javascript// Lambda/Functions com PgBouncer transaction pooling
const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.PGBOUNCER_HOST,
  port: 6432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 5, // Pool pequeno por função (muitas funções = muitas conexões)
  idleTimeoutMillis: 10000,
  connectionTimeoutMillis: 2000
});

// Handler de função serverless
exports.handler = async (event) => {
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users WHERE id = $1', [event.userId]);
    return result.rows[0];
  } finally {
    client.release();
  }
};

Read replicas com pooling:

javascript// Routing para replicas (primary para writes, replicas para reads)
const primaryPool = new Pool({
  host: 'primary.db.internal',
  port: 5432,
  max: 50
});

const replicaPools = [
  new Pool({ host: 'replica1.db.internal', port: 5432, max: 50 }),
  new Pool({ host: 'replica2.db.internal', port: 5432, max: 50 }),
  new Pool({ host: 'replica3.db.internal', port: 5432, max: 50 })
});

// Routing logic
function getPool(isWrite = false) {
  if (isWrite) return primaryPool;

  // Round-robin simples
  const index = Math.floor(Math.random() * replicaPools.length);
  return replicaPools[index];
}

// Query com routing
async function query(sql, params, isWrite = false) {
  const pool = getPool(isWrite);
  const client = await pool.connect();
  try {
    return await client.query(sql, params);
  } finally {
    client.release();
  }
}

Trade-offs e anti-padrões

Anti-padrão: Pool sem limites

javascript// ❌ Pool sem max
const pool = new Pool({ host: 'localhost' }); // max = 10, mas pode sobrecarregar
javascript// ✅ Pool com limites explícitos
const pool = new Pool({
  host: 'localhost',
  max: 20, // Explicito e alinhado com cores
  idleTimeoutMillis: 30000
});

Anti-padrão: Pool muito grande

javascript// ❌ Pool gigante em máquina pequena
const pool = new Pool({
  max: 500 // Excede capacidade do servidor
});
javascript// ✅ Pool alinhado com capacidade
const pool = new Pool({
  max: os.cpus().length * 2 // Alinhado com recursos
});

Anti-padrão: Sem timeout de checkout

javascript// ❌ Espera infinita por conexão
const pool = new Pool({
  host: 'localhost',
  max: 20
});

// Se pool está exausto, espera indefinidamente
javascript// ✅ Timeout explícito
const pool = new Pool({
  host: 'localhost',
  max: 20,
  connectionTimeoutMillis: 5000 // Falha rápido se pool exausto
});

Métricas de sucesso

Para pools de aplicação:

  • Pool utilization: 70-90% (suficiente uso, sem exaustão)
  • Checkout latency P99: <10ms para obter conexão do pool
  • Checkout failures: <0.1% (falhas ao obter conexão)
  • Connection churn: <5% de conexões abertas/fechadas por minuto

Para PgBouncer:

  • Pool utilization: 80-95% (uso eficiente de conexões PostgreSQL)
  • Rejections por pool exaustão: <0.01%
  • Latência de query adicional vs. direto: <5ms overhead
  • Database connections: <max_connections com buffer de 20%

Plano de execução em fases

Fase 1: Baseline e diagnóstico (1 semana)

  1. Medir conexões atuais e latências de query
  2. Identificar hotspots de connection contention
  3. Documentar arquitetura atual (app pools, proxies, banco)

Fase 2: Implementação de pool otimizado (2-4 semanas)

  1. Implementar pool de aplicação com size correto (cores × 2)
  2. Configurar PgBouncer se workload for multi-instância
  3. Implementar monitoramento de pool utilization e latência

Fase 3: Tuning e validação (2-4 semanas)

  1. Executar testes de carga com configuração otimizada
  2. Ajustar pool size baseado em métricas reais
  3. Validar escalabilidade horizontal (múltiplas instâncias)
  4. Documentar SLAs de connection pooling

Sua aplicação está sofrendo com timeouts de banco ou latência imprevisível? Fale com especialistas da Imperialis sobre otimização de connection pooling que garante performance consistente em escala.

Fontes

Leituras relacionadas