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();import os
import threading
import time
from psycopg_pool import ConnectionPool, PoolTimeout
pool = ConnectionPool(
os.environ["DATABASE_URL"],
min_size=1,
max_size=5, # conexões no máximo, por instância da aplicação
timeout=1.0, # quanto um pedido espera por uma conexão livre
max_idle=30, # fecha conexões paradas há mais de 30 s
max_lifetime=1800, # renova cada conexão a cada 30 min
open=True,
)
def consultar(sql: str) -> str:
inicio = time.monotonic()
try:
with pool.connection() as conexao: # devolve a conexão ao sair, mesmo com erro
conexao.execute(sql)
return f"ok em {(time.monotonic() - inicio) * 1000:.0f} ms"
except PoolTimeout as erro:
return f"falhou em {(time.monotonic() - inicio) * 1000:.0f} ms: {erro}"
# Cinco consultas lentas ocupam o pool inteiro; a sexta não espera para sempre
lentas = [threading.Thread(target=consultar, args=("SELECT pg_sleep(3)",)) for _ in range(5)]
for t in lentas:
t.start()
time.sleep(0.2)
print("sexta, com o pool cheio:", consultar("SELECT 1"))
for t in lentas:
t.join()
print("depois que liberaram:", consultar("SELECT 1"))
pool.close()package main
import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func novoPool(ctx context.Context) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
return nil, err
}
config.MaxConns = 5 // conexões no máximo, por instância da aplicação
config.MaxConnIdleTime = 30 * time.Second // fecha conexões paradas há mais de 30 s
config.MaxConnLifetime = 30 * time.Minute // renova cada conexão a cada 30 min
return pgxpool.NewWithConfig(ctx, config)
}
// consultar limita a espera pela conexão (e pela consulta) com o prazo do contexto
func consultar(db *pgxpool.Pool, sql string, espera time.Duration) string {
ctx, cancelar := context.WithTimeout(context.Background(), espera)
defer cancelar()
inicio := time.Now()
if _, err := db.Exec(ctx, sql); err != nil { // Exec devolve a conexão ao terminar
return fmt.Sprintf("falhou em %d ms: %v", time.Since(inicio).Milliseconds(), err)
}
return fmt.Sprintf("ok em %d ms", time.Since(inicio).Milliseconds())
}
func main() {
db, err := novoPool(context.Background())
if err != nil {
panic(err)
}
defer db.Close()
// Cinco consultas lentas ocupam o pool inteiro; a sexta não espera para sempre
var wg sync.WaitGroup
for range 5 {
wg.Go(func() { consultar(db, "SELECT pg_sleep(3)", 10*time.Second) })
}
time.Sleep(200 * time.Millisecond)
fmt.Println("sexta, com o pool cheio:", consultar(db, "SELECT 1", time.Second))
wg.Wait()
fmt.Println("depois que liberaram:", consultar(db, "SELECT 1", time.Second))
}import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.concurrent.Executors;
public class Pool {
static HikariDataSource novoPool() {
var config = new HikariConfig();
config.setJdbcUrl(System.getenv("JDBC_URL"));
config.setMaximumPoolSize(5); // conexões no máximo, por instância da aplicação
config.setConnectionTimeout(1000); // quanto um pedido espera por uma conexão livre (ms)
config.setIdleTimeout(30_000); // fecha conexões paradas há mais de 30 s
config.setMaxLifetime(1_800_000); // renova cada conexão a cada 30 min
return new HikariDataSource(config);
}
static String consultar(HikariDataSource pool, String sql) {
long inicio = System.currentTimeMillis();
// try-with-resources devolve a conexão ao pool, mesmo com erro
try (var conexao = pool.getConnection(); var comando = conexao.createStatement()) {
comando.execute(sql);
return "ok em " + (System.currentTimeMillis() - inicio) + " ms";
} catch (SQLException erro) {
long ms = System.currentTimeMillis() - inicio;
return "falhou em " + ms + " ms: " + erro.getMessage();
}
}
public static void main(String[] args) throws Exception {
try (var pool = novoPool(); var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Cinco consultas lentas ocupam o pool inteiro; a sexta não espera para sempre
var lentas = new ArrayList<java.util.concurrent.Future<String>>();
for (int i = 0; i < 5; i++) {
lentas.add(executor.submit(() -> consultar(pool, "SELECT pg_sleep(3)")));
}
Thread.sleep(200);
System.out.println("sexta, com o pool cheio: " + consultar(pool, "SELECT 1"));
for (var l : lentas) l.get();
System.out.println("depois que liberaram: " + consultar(pool, "SELECT 1"));
}
}
}using System.Diagnostics;
using Npgsql;
// No Npgsql, o pool é configurado na string de conexão
var url = Environment.GetEnvironmentVariable("DATABASE_URL");
var construtor = new NpgsqlConnectionStringBuilder(url)
{
MaxPoolSize = 5, // conexões no máximo, por instância da aplicação
Timeout = 1, // segundos de espera por uma conexão livre
ConnectionIdleLifetime = 30, // fecha conexões paradas há mais de 30 s
ConnectionLifetime = 1800, // renova cada conexão a cada 30 min
};
await using var db = NpgsqlDataSource.Create(construtor.ConnectionString);
async Task<string> Consultar(string sql)
{
var relogio = Stopwatch.StartNew();
try
{
await using var comando = db.CreateCommand(sql); // pega e devolve a conexão sozinho
await comando.ExecuteNonQueryAsync();
return $"ok em {relogio.ElapsedMilliseconds} ms";
}
catch (NpgsqlException erro)
{
return $"falhou em {relogio.ElapsedMilliseconds} ms: {erro.Message}";
}
}
// Cinco consultas lentas ocupam o pool inteiro; a sexta não espera para sempre
var lentas = Enumerable.Range(0, 5).Select(_ => Consultar("SELECT pg_sleep(3)")).ToList();
await Task.Delay(200);
Console.WriteLine($"sexta, com o pool cheio: {await Consultar("SELECT 1")}");
await Task.WhenAll(lentas);
Console.WriteLine($"depois que liberaram: {await Consultar("SELECT 1")}");O resultado, nas cinco linguagens
| Sexta consulta, com o pool cheio | Mensagem | Depois que liberaram | |
|---|---|---|---|
TypeScript (pg) | falhou em 1.002 ms | timeout exceeded when trying to connect | ok em 3 ms |
Python (psycopg_pool) | falhou em 1.001 ms | couldn't get a connection after 1.00 sec | ok em 2 ms |
Go (pgxpool) | falhou em 1.000 ms | context deadline exceeded | ok em menos de 1 ms |
| Java (HikariCP) | falhou em 1.040 ms | Connection 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 ms | The 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áximo | Espera por conexão | Ociosa | Vida máxima | |
|---|---|---|---|---|
| TypeScript | max | connectionTimeoutMillis | idleTimeoutMillis | maxLifetimeSeconds |
| Python | max_size | timeout | max_idle | max_lifetime |
| Go | MaxConns | prazo do context | MaxConnIdleTime | MaxConnLifetime |
| Java | maximumPoolSize | connectionTimeout | idleTimeout | maxLifetime |
| C# | Maximum Pool Size | Timeout | Connection Idle Lifetime | Connection 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 = 1000O 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 LOCALe 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]