PgBouncer: connection pooling para PostgreSQL

Cada conexão ao PostgreSQL custa ~5 MB de RAM e um processo no SO. APIs Node.js com 10 instâncias e pool de 20 conexões cada geram 200 conexões simultâneas, o suficiente para derrubar um servidor. PgBouncer multiplexeia centenas de conexões da aplicação em poucos processos do PostgreSQL.

Instalar PgBouncer

Instalar e configurar o PgBouncer como proxy de conexões:

bash
# Instalar PgBouncer:
sudo apt install pgbouncer -y

# Verificar versão:
pgbouncer --version

# Arquivos de configuração:
# /etc/pgbouncer/pgbouncer.ini  — configuração principal
# /etc/pgbouncer/userlist.txt   — usuários e senhas

# pgbouncer.ini — configuração básica:
sudo tee /etc/pgbouncer/pgbouncer.ini << 'EOF'
[databases]
# Mapear banco "producao" para o PostgreSQL local:
producao = host=127.0.0.1 port=5432 dbname=producao

[pgbouncer]
listen_port = 5433          # porta do PgBouncer (diferente do Postgres)
listen_addr = 127.0.0.1
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid

# Modo de pooling:
pool_mode = transaction     # recomendado para APIs

# Limites de conexões:
max_client_conn = 1000      # conexões de clientes (aplicações)
default_pool_size = 20      # conexões para o PostgreSQL por banco

# Timeouts:
client_idle_timeout = 600
server_idle_timeout = 600
EOF

# Criar arquivo de usuários:
# Gerar hash da senha (md5):
echo -n "senhaPGbouncer" | md5sum
# Formato: "usuario" "md5HASH"
echo '"app_user" "md5HASH_AQUI"' | sudo tee /etc/pgbouncer/userlist.txt

sudo systemctl enable pgbouncer
sudo systemctl restart pgbouncer

Modos de pooling: session, transaction e statement

Escolher o modo certo para sua aplicação:

bash
# pgbouncer.ini — comparação dos modos:

# SESSION pooling (menos eficiente):
# pool_mode = session
# Uma conexão do PostgreSQL fica alocada por toda a sessão do cliente.
# Compatível com TODAS as funcionalidades do PostgreSQL.
# Use para: aplicações que usam SET, prepared statements nomeados, cursors.

# TRANSACTION pooling (recomendado para APIs):
# pool_mode = transaction
# A conexão é liberada ao pool após cada COMMIT/ROLLBACK.
# Uma conexão do PostgreSQL serve múltiplos clientes (round-robin).
# NÃO compatível com: SET SESSION, prepared statements nomeados (use $1 params),
# LISTEN/NOTIFY, cursors que vivem entre transações.
# Use para: APIs REST stateless, onde cada requisição é uma transação.

# STATEMENT pooling (mais restritivo):
# pool_mode = statement
# Libera a conexão após cada statement (cada query).
# NÃO compatível com transações multi-statement.
# Use para: SELECT simples sem transações.

# Para Node.js com pg ou Prisma: transaction mode é ideal.
# Configurar no DATABASE_URL:
# postgresql://usuario:senha@localhost:5433/producao
#                                       ^^^^ porta do PgBouncer (não 5432)

# Preparar a aplicação para transaction mode:
# Não usar: SET search_path, PREPARE nome_stmt, DECLARE cursor
# Usar sempre: prepared statements sem nome ($1, $2) ou queries inline

Configurar a aplicação Node.js para usar PgBouncer

Ajustar o pool da aplicação para trabalhar com PgBouncer:

typescript
// Com PgBouncer, reduzir o pool da aplicação:
import { Pool } from 'pg'

const db = new Pool({
  connectionString: process.env.DATABASE_URL,
  // PgBouncer já faz pooling — pool da app deve ser pequeno:
  max: 5,              // máximo de 5 conexões simultâneas por instância
  idleTimeoutMillis: 10_000,
  connectionTimeoutMillis: 5_000,
  // IMPORTANTE para transaction mode:
  // Não usar prepared statements nomeados:
  statement_timeout: 30_000,
})

// Prisma — adicionar pgbouncer=true no connection string:
// DATABASE_URL="postgresql://user:pass@localhost:5433/db?pgbouncer=true"
// O Prisma desativa prepared statements nomeados automaticamente

// Drizzle ORM — sem configuração especial necessária
// (usa prepared statements sem nome por padrão)

// TypeORM — desativar prepared statements:
// const ds = new DataSource({
//   type: 'postgres',
//   url: process.env.DATABASE_URL,
//   extra: { prepareCached: false }
// })

// Verificar conexões ativas no PgBouncer:
// psql -h localhost -p 5433 -U pgbouncer pgbouncer -c "SHOW POOLS;"
// psql -h localhost -p 5433 -U pgbouncer pgbouncer -c "SHOW CLIENTS;"
// psql -h localhost -p 5433 -U pgbouncer pgbouncer -c "SHOW SERVERS;"

PgBouncer com Docker Compose

Configurar PgBouncer como serviço Docker junto ao PostgreSQL:

yaml
# docker-compose.yml — PostgreSQL + PgBouncer:
services:
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: producao
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - pg_data:/var/lib/postgresql/data

  pgbouncer:
    image: edoburu/pgbouncer:latest
    restart: always
    ports:
      - "127.0.0.1:5433:5432"
    environment:
      DATABASE_URL: "postgres://app_user:${POSTGRES_PASSWORD}@postgres:5432/producao"
      POOL_MODE: transaction
      MAX_CLIENT_CONN: 1000
      DEFAULT_POOL_SIZE: 25
      AUTH_TYPE: scram-sha-256
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  pg_data:

# A aplicação conecta em localhost:5433 (PgBouncer)
# PgBouncer conecta em postgres:5432 (PostgreSQL interno)
# DATABASE_URL=postgresql://app_user:senha@localhost:5433/producao

Monitorar PgBouncer e métricas de pool

Acompanhar uso de conexões e detectar gargalos:

bash
# Conectar ao console admin do PgBouncer:
psql -h 127.0.0.1 -p 5433 -U pgbouncer pgbouncer

# Comandos úteis no console admin:
SHOW POOLS;
-- pool_mode | cl_active | cl_waiting | sv_active | sv_idle | sv_used
-- cl_active:  clientes com conexão ativa no PostgreSQL
-- cl_waiting: clientes esperando por uma conexão (GARGALO!)
-- sv_active:  conexões do PostgreSQL em uso
-- sv_idle:    conexões do PostgreSQL ociosas no pool

SHOW CLIENTS;   -- todas as conexões de clientes
SHOW SERVERS;   -- todas as conexões para o PostgreSQL
SHOW STATS;     -- estatísticas de uso (requests/s, latência)

# Métricas via Prometheus (pgbouncer_exporter):
docker run -d --name pgbouncer-exporter   -p 127.0.0.1:9127:9127   -e DATABASE_URL="postgresql://pgbouncer:senha@localhost:5433/pgbouncer"   prometheuscommunity/pgbouncer-exporter

# prometheus.yml — adicionar scrape:
# - job_name: pgbouncer
#   static_configs:
#     - targets: ['localhost:9127']

# Alertas importantes:
# cl_waiting > 0 por mais de 30s → pool insuficiente → aumentar default_pool_size
# sv_active ≈ default_pool_size → pool saturado → aumentar ou otimizar queries

Não quer configurar manualmente?

Implante o VPS para APIs em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Depende da escala. Para 1 instância com pool de 10 conexões: provavelmente não. Para múltiplas instâncias (PM2 cluster, vários containers): sem PgBouncer, cada instância mantém seu próprio pool e o total de conexões no PostgreSQL cresce linearmente. Com PgBouncer: todas as instâncias compartilham o mesmo pool, o PostgreSQL vê no máximo default_pool_size conexões independente de quantas instâncias rodarem.

Erros comuns: "prepared statement already exists", Prisma sem pgbouncer=true na URL; "SET command", aplicação usando SET SESSION; "cursor does not exist", cursor aberto entre transações. Para depurar: ative pgbouncer.log em verbose e observe quais queries falham. A solução geralmente é: adicionar pgbouncer=true no Prisma, ou usar session mode para aplicações incompatíveis.

Regra prática: default_pool_size = (2 × núcleos de CPU do servidor PostgreSQL) + discos de armazenamento efetivos. Para VPS de 4 vCPUs: ~10-20 conexões reais são suficientes para saturar a CPU. Aumentar além disso piora a performance por context switching. Monitorar: se cl_waiting > 0 persistentemente, aumentar pool ou otimizar queries lentas.

PgBouncer: focado exclusivamente em connection pooling, muito leve (< 20 MB RAM), simples de configurar. Pgpool-II: pooling + load balancing + failover automático + query cache + replicação, muito mais complexo. Para pooling simples: PgBouncer. Para arquitetura com réplica de leitura e failover automático: Pgpool-II (ou Patroni + HAProxy). Para a maioria das VPS single-node: PgBouncer é suficiente.

SCRAM-SHA-256: autenticação mais segura, suportado desde PostgreSQL 10 e PgBouncer 1.14. MD5: legado, ainda funciona, mas hash reversível, vulnerável a ataques de dicionário se o arquivo userlist.txt vazar. Use SCRAM-SHA-256 em novas instalações. Para PostgreSQL 14+: configure password_encryption = scram-sha-256 no postgresql.conf e recrie os usuários com a nova criptografia.