PRODPlataforma BaaS europeia soberanaAbra o painel →

Desempenho · 10 minutos de leitura

PgBouncer pool de transações explicado

Affane Daylami · Fondateur · 12 de junho de 2026

Voltar ao blog

O modo de transação do PgBouncer libera a conexão PostgreSQL ao final de cada transação, não quando o cliente se desconecta. Isso é o que torna possível atender milhares de clientes HTTP com algumas dezenas de conexões reais de servidor e é o modo recomendado para qualquer API REST de consulta curta.

Este texto em inglês foi gerado automaticamente a partir do original em francês e ainda não foi revisado.
Esta página foi traduzida automaticamente. A versão em inglês é oficial.

Esse ganho tem um custo específico: o modo de transação quebra silenciosamente tudo o que pressupõe uma conexão Postgres estável de uma solicitação para outra. Sessão SET, LISTEN/NOTIFY, bloqueios de aviso, cursores que sobrevivem à transação, chamados de instruções preparadas. Este artigo detalha o mecanismo, lista esses limites com seus sintomas exatos e depois mostra como um backend em produção (o nosso, verificado diretamente em seu repositório) o configura sem ser preso por ele. Para o método de medição por trás de quaisquer números de desempenho citados aqui, consulte nossa metodologia de benchmark .

O essencial

  • Modo transação: a conexão do servidor é liberada ao final de cada transação, e não quando o cliente se desconecta. Este é o modo mais eficiente para compartilhar conexões curtas do tipo API REST.
  • Incompatível por construção com: sessão SET/RESET, LISTEN/NOTIFY, bloqueios de aconselhamento de sessão, cursores WITH HOLD, tabelas temporárias reutilizadas de uma solicitação para outra.
  • A armadilha mais comum na prática: instruções preparadas nomeadas, que vários drivers (sqlx, asyncpg, o driver JDBC pgjdbc) ativam por padrão, podem ser reproduzidas em uma conexão de servidor diferente e disparar um erro como prepared statement does not exist sob carga.
  • Desde a versão 1.21, o PgBouncer pode seguir instruções preparadas por protocolo em modo de transação (cache LRU via conexão do servidor). Isso não impede a desativação do cache do lado do cliente se seu aplicativo alterar search_path em cada solicitação.
  • Verificado no código Aurabase: os pools de locatários são executados com statement_cache_capacity(0) e PgBouncer em pool_mode=transaction, enquanto PostgREST permanece voluntariamente em conexão direta para recarregar seu esquema via LISTEN/NOTIFY.
#
Conceitos

Os 3 modos de pool do PgBouncer

O PgBouncer oferece três modos, que diferem apenas quando a conexão do servidor Postgres retorna ao pool comum. A documentação oficial os nomeia session, transaction e statement (pgbouncer.org/features.html, seção “Modos de pooling”, acessado em 24 de agosto de 2026).

ModaConexão do servidor perdidaCompatibilidade de sessão
sessão (padrão)Na desconexão do clienteTotal: SET, LISTEN, cursores, tudo funciona como ao vivo
transaçãoNo final de cada transação (COMMIT/ROLLBACK)Parcial: apenas o que permanece local para a transação
declaraçãoApós cada solicitação individualMínimo: transações explícitas de múltiplas consultas proibidas

O modo de sessão é o mais permissivo, mas o menos eficaz em escalabilidade: uma conexão Postgres permanece reservada para um cliente enquanto permanecer conectado, mesmo que não faça nada entre duas solicitações. O modo de instrução é reservado para casos muito específicos (proxy somente leitura, verificações de integridade) e até quebra transações explícitas clássicas. O modo de transação é o compromisso que domina na prática para uma API REST: cada solicitação HTTP geralmente corresponde a uma única transação curta do Postgres.

#
Mecanismo

Como funciona o modo de transação, conexão por conexão

No modo de transação, o PgBouncer apenas conecta uma conexão de servidor a um cliente quando este abre uma transação e a retorna ao pool após COMMIT ou ROLLBACK. Entre duas transações, o mesmo cliente pode ser transferido para uma conexão de servidor completamente diferente.

Concretamente, com um default_pool_size de 20, o PgBouncer pode absorver várias centenas de clientes simultâneos que, a qualquer momento, têm apenas algumas transações em andamento. É esta relação que justifica o modo de transação para uma API REST com alto tráfego, mas transações curtas: o recurso raro (uma conexão Postgres, cara em memória do lado do servidor) só é ocupado pelo tempo estritamente necessário.

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

Este último comentário resume a essência: o modo de transação funciona porque quebra deliberadamente o link entre "minha sessão de aplicativo" e "minha conexão Postgres". Tudo baseado neste link quebra. A próxima seção lista exatamente o quê.

#
Limites

O que quebra no modo de pooling de transações

A documentação oficial do PgBouncer lista explicitamente os recursos do PostgreSQL que perdem seu significado assim que uma conexão de servidor pode ser reciclada entre duas solicitações do mesmo cliente.

Funcionalidade afetadaPor que isso quebraSintoma típico
DEFINIR / DEFINIR SESSÃOA configuração se aplica a uma conexão que pode ser reciclada imediatamente apósUm parâmetro parece ser esquecido aleatoriamente entre duas solicitações
OUÇA / NOTIFIQUEAssume uma conexão persistente para receber notificaçõesO cliente nunca é notificado ou apenas intermitentemente
Bloqueios de aconselhamento de sessãoO bloqueio é mantido pela conexão do servidor, não pelo cliente lógicoUm bloqueio é liberado antes da conclusão esperada ou nunca é liberado
COM controles deslizantes HOLDDeve sobreviver além da transação que o abriuErro "cursor não existe" na próxima iteração
Tabelas temporáriasRelacionado à sessão do Postgres, não à transaçãoA tabela "desaparece" na próxima consulta
Declarações preparadas nomeadasPreparado em uma conexão de servidor específica, reproduzido em outro"declaração preparada... não existe" sob carga
A armadilha nem sempre é imediata

A maioria destas limitações não se manifesta no desenvolvimento local, onde uma única ligação normalmente serve todo o tráfego. Eles aparecem sob carga real, quando vários clientes realmente compartilham o pool e uma conexão de servidor realmente muda de mãos entre duas solicitações do mesmo cliente lógico. Um teste de fumaça quase nunca os revela.

#
Armadilha comum

Declarações preparadas: o limite mais incompreendido

A maioria dos drivers Postgres modernos prepara solicitações nomeadas no lado do protocolo por padrão, sem que o código do aplicativo solicite explicitamente. Isto é precisamente o que torna esta armadilha difícil de prever.

Uma instrução preparada por protocolo é nomeada e armazenada em cache em uma conexão de servidor específica, no momento de Parse. No modo transação, esta conexão pode ser reatribuída a outro cliente entre duas solicitações do mesmo cliente lógico. Se o driver reproduzir o mesmo nome de instrução em uma conexão onde nunca foi preparado, o Postgres responderá com um erro explícito, normalmente prepared statement "sqlx_s_N" does not exist para um cliente sqlx. O comportamento é intermitente: depende do desempenho das conexões sob carga, e não de um bug determinístico reproduzível em cada chamada.

A correção do lado do cliente é a mesma, independentemente do idioma: desabilite o cache de instruções preparadas nomeadas ou force consultas não nomeadas para qualquer pool que cruze um pooler no modo de transação. No Rust com sqlx, passa por statement_cache_capacity(0) nas opções de conexão.

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// Equivalentes: asyncpg -> Statement_cache_size=0, pgjdbc -> prepareThreshold=0

Desde a versão 1.21, o PgBouncer alivia parte do problema do lado do servidor: ele pode seguir instruções preparadas por protocolo em modo de transação e prepará-las dinamicamente na conexão atribuída, com um cache LRU por conexão cujo tamanho é ajustado via max_prepared_statements. Isso reduz o número de falhas, mas não o isenta de desabilitar o cache do cliente em um pool multilocatário onde o search_path muda de uma solicitação para outra: um plano em cache congela o identificador interno (OID) da tabela resolvida no momento de Parse, e reproduzi-lo sob outro esquema pode retornar dados do locatário errado em vez de um simples erro.

#
Verificado no código

Como o Aurabase configura o PgBouncer em modo de transação

O repositório Aurabase implanta PgBouncer como pool_mode=transaction na frente do plano de dados compartilhado (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) e um Pooler CNPG configurado de forma idêntica na frente de cada instância Postgres dedicada de um locatário (deploy/cnpg/tenant-pooler.yaml). Ambos os caminhos aplicam a mesma disciplina descrita acima.

O código-fonte documenta um motivo de segurança específico para esta escolha, não apenas um motivo de estabilidade. Os pools Postgres compartilhados entre locatários posicionam um search_path diferente por projeto em conexões reutilizadas. Uma instrução preparada em cache congela o OID da tabela resolvida no momento de Parse; reproduzi-lo para outro locatário na mesma conexão executaria a consulta no esquema do primeiro locatário, um desvio de isolamento, não apenas um erro de aplicativo. statement_cache_capacity(0) é, portanto, aplicado sem exceção, inclusive em instâncias dedicadas que também passam por um Pooler CNPG em modo de transação.

Medida de higiene da segunda sessão: quando cada conexão retorna ao pool, um gancho executa DISCARD ALL (redefinindo configurações, desalocando instruções preparadas no lado do servidor, liberando bloqueios de aconselhamento, limpando cursores e tabelas temporárias). Sem esse gancho, um resíduo de sessão colocado por uma solicitação anterior poderia vazar na próxima solicitação de um locatário diferente, reutilizando a mesma conexão reciclada.

Exceção aceita: instâncias PostgREST dedicadas permanecem em conexão direta com o primário, sem passar pelo pooler. O recarregamento do esquema PostgREST depende de LISTEN/NOTIFY, que assume uma conexão persistente, exatamente a funcionalidade que o modo de transação quebra (detalhe já documentado em nosso artigo sobre Compatibilidade do PostgREST no Aurabase). As configurações de RLS por solicitação são passadas para SET LOCAL dentro de uma transação explícita, a única maneira de permanecer compatível com um pool que pode alterar as conexões do servidor em qualquer COMMIT (consulte nosso artigo sobreisolamento RLS multilocatário).

#
Guia prático

Habilite o modo de transação sem interromper seu aplicativo

Uma pequena lista de verificação, aplicável a qualquer back-end que esteja mudando de uma conexão direta do Postgres para um PgBouncer no modo de transação.

  1. Audite o código do aplicativo. Procure SET, LISTEN/NOTIFY, bloqueios de aconselhamento de sessão, cursores WITH HOLD e tabelas temporárias reutilizadas entre consultas.
  2. Substitua SETs de sessão por SETs LOCAL em uma transação explícita. Esta é a única configuração que sobrevive adequadamente à reciclagem de conexão, porque ela é limpa em COMMIT/ROLLBACK em vez de vazar na próxima conexão.
  3. Desative o cache de instruções preparadas do lado do driver se o seu conjunto atravessar o pooler e o esquema ou a função mudar de uma solicitação para outra. O custo do desempenho é real, mas mensurável, e muito inferior ao risco de fuga entre inquilinos.
  4. Isole as conexões que realmente precisam do modo de sessão (migrações, scripts administrativos, qualquer coisa que dependa de LISTEN/NOTIFY) para uma conexão direta sem pooler, em vez de renunciar ao modo de transação para todos os outros tráfegos.
  5. Tamanho default_pool_size e max_client_conn em relação ao max_connectionsreal do Postgres, não por uma figura arbitrária copiada de outro projeto.
  6. Teste sob carga real, não apenas teste de fumaça. Erros de instrução preparada e vazamentos de configurações de sessão quase nunca aparecem em uma única conexão local.
  7. Monitore SHOW POOLS e SHOW STATS no console de administração do PgBouncer uma vez em produção, para detectar a saturação do pool antes que ela se torne visível no lado do cliente.
#
Decisão

Você deve sempre escolher o modo de transação em vez de sessão?

Não, mas é a escolha padrão correta para a grande maioria das APIs REST. O modo de sessão permanece preferível para um aplicativo legado fortemente dependente de funcionalidades de sessão que você não pode refatorar rapidamente ou para tráfego baixo onde o ganho de pooling não compensa o esforço de migração.

O PgBouncer também não é a única implementação deste modelo de pooling: Supavisor (Supabase) e PgCat são duas alternativas recentes, com diferentes compensações na distribuição de carga e clustering. Veja nossa comparação detalhada, PgBouncer vs Supavisor vs PgCat, para escolher entre os três dependendo de sua topologia.

#
Perguntas frequentes

Perguntas frequentes

As perguntas que surgem com mais frequência quando o modo de transação é ativado na produção.

O que é o modo de pool de transações PgBouncer?+
Este é um dos 3 modos do PgBouncer (com sessão e instrução) em que a conexão Postgres é reatribuída a outro cliente ao final de cada transação, e não quando o cliente se desconecta. Isso torna possível atender muito mais clientes concorrentes do que o número real de conexões abertas do Postgres.
Por que meus extratos preparados travam no modo de transação?+
Uma instrução preparada nomeada é preparada em uma conexão de servidor específica. No modo transação, esta conexão pode ser reatribuída a outro cliente entre duas solicitações. Se o seu driver reproduzir o nome da instrução em uma conexão onde ela nunca foi preparada, o Postgres retornará um erro como a instrução preparada não existe. A correção consiste em desabilitar o cache de instruções preparadas no lado do driver (statement_cache_capacity(0) com sqlx, Statement_cache_size=0 com asyncpg).
Podemos usar LISTEN/NOTIFY atrás de um PgBouncer no modo de transação?+
Não, não de forma confiável. LISTEN/NOTIFY assume uma conexão persistente para receber notificações, cujo modo de transação não garante. A prática padrão é passar componentes que dependem de LISTEN/NOTIFY (PostgREST, por exemplo) através de uma conexão direta com o Postgres, fora do pooler.
Devemos usar SET LOCAL em vez de SET no modo de transação?+
Sim, sistematicamente para qualquer configuração que deva ser aplicada a uma determinada solicitação. SET LOCAL é limpo automaticamente em COMMIT ou ROLLBACK, tornando-o seguro com uma conexão de servidor que pode mudar entre duas transações. Um SET clássico pode vazar para o próximo cliente que recupera a mesma conexão de servidor reciclada.
O modo de transação funciona com Row Level Security (RLS)?+
Sim, desde que as declarações JWT ou variáveis de sessão usadas pelas suas políticas RLS sejam definidas em LOCAL SET dentro da transação, e não na sessão SET. Este é o padrão descrito em nosso artigo sobre isolamento RLS multilocatário.
PgBouncer, Supavisor, PgCat: qual escolher?+
Os três implementam um modelo de pooling semelhante, com diferenças em clustering, distribuição de carga e ecossistema (Supavisor é desenvolvido pela Supabase, PgCat é escrito em Rust). A escolha depende acima de tudo da sua topologia de implantação e das restrições operacionais existentes: consulte nossa comparação dedicada para obter detalhes.

PRONTO PARA IMPLEMENTAR?

Seu back-end em cinco minutos.

Não é necessário cartão de crédito · 500 MB grátis · 50.000 MAU