PgBouncer: ajustar server_lifetime corta uso de memória do PostgreSQL pela metade
Um caso publicado por Dinesh Kumar, da Cortex Ridge, mostra como um banco multi-tenant com 82 mil tabelas enchia a memória do PostgreSQL porque o pooler mantinha conexões abertas por 24 horas. A correção foi trocar uma única configuração do PgBouncer.

- Leitura
- 6 min
- Código
- 2
- Fontes
- 1
Um caso publicado por Dinesh Kumar, da Cortex Ridge, mostra como um banco multi-tenant com 82 mil tabelas enchia a memória do PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → porque o pooler mantinha conexões abertas por 24 horas. A correção foi trocar uma única configuração do PgBouncer.
A tentação, diante de um alerta de memória, é sempre a mesma: aumentar o servidor. Foi o caminho oposto que resolveu o problema descrito por Dinesh Kumar, da consultoria Cortex Ridge, num post publicado em 15 de novembro de 2026 e replicado no agregador Planet PostgreSQL. O cliente recebia alertas de memória em 93%, 92% e 91% num único dia, e a correção não foi comprar mais RAM: foi cortar pela metade a memória da instância e mudar uma única configuração do pooler de conexões.
O problema nasceu da arquitetura multi-tenant
O banco em questão usa o padrão schema-por-tenant: cada cliente final tem seu próprio schema dentro do mesmo PostgreSQL. Na escala descrita pela Cortex Ridge, isso significa 432 schemas e 82.058 tabelas num único banco. É uma escolha de modelagem legítima para isolar dados entre tenants, mas ela tem uma consequência que passa despercebida até a base crescer: cada conexão que transita entre tenants acaba tocando milhares de tabelas diferentes ao longo do dia.
Em um banco comum, com algumas centenas de tabelas, ninguém nota esse efeito. O volume aqui é que expõe um comportamento interno do PostgreSQL que normalmente fica invisível.
O cache de catálogo é por conexão, e o PostgreSQL nunca o esvazia
Todo backend↳Back-end49 conteúdosIntegração front-end com backend: 7 decisões que evitam caos entre APIs, BFF e GraphQLDev (Back & Front) · abr 2026Como criar uma FAKE API REST para testes — JSONPlaceholderDev (Back & Front) · set 2025Construindo um aplicativo de bate-papo de IA simples com Spring AI e AngularDev (Back & Front) · jul 2025Ver tudo em Dev (Back & Front) → do PostgreSQL mantém, na própria memória de processo, um cache privado das tabelas, colunas, índices e constraints que já consultou. Esse cache de catálogo existe para evitar reconsultas constantes ao dicionário de dados, e isso é correto do ponto de vista de performance de query: sem ele, cada SELECT pagaria o custo de reler metadados do sistema.
O detalhe que a Cortex Ridge destaca, e que todo DBA deveria ter internalizado, é que esse cache não é liberado automaticamente. Ele só é descartado quando a conexão fecha. Numa conexão de aplicação comum, que atende poucos esquemas durante sua vida, isso nunca vira problema. Numa conexão de pooler que serve dezenas de tenants por dia, o cache cresce sem limite enquanto a conexão existir.
O diagnóstico: o pooler segurava conexões por 24 horas
As aplicações do cliente não se conectavam direto ao PostgreSQL: passavam por um connection pooler, que mantém um conjunto de conexões abertas com o banco e as compartilha entre clientes. Essa é a arquitetura recomendada para qualquer PostgreSQL com muitas conexões simultâneas, porque abrir e fechar conexão com o Postgres tem custo de CPU e de handshake.
Reduzimos a memória da instância PostgreSQL pela metade e mudamos uma configuração do pooler. Desde então não recebem mais alertas.
No original: We cut their PostgreSQL instance memory in half and changed one connection pooler setting. They have not had a memory alert since.
O problema era a configuração de tempo de vida dessas conexões: o pooler as mantinha abertas por 24 horas. Cada conexão no pool acumulava, ao longo do dia, o cache de catálogo de centenas ou milhares de tabelas diferentes, e o conjunto do pool passou a consumir a maior parte da memória disponível no servidor.
A correção: encurtar o server_lifetime
A mudança foi reduzir o tempo de vida de cada conexão no pooler para 10 minutos. No PgBouncer, que é o pooler citado no caso, esse parâmetro é o server_lifetime, configurado em segundos:
[pgbouncer]
server_lifetime = 600 ; 10 minutos (o padrão do PgBouncer é 3600, uma hora)Vale notar que o valor de 24 horas usado pelo cliente já era um desvio grande do padrão do próprio PgBouncer, que é de uma hora. Quando uma conexão atinge esse limite de idade, o pooler a fecha e abre uma nova assim que for necessária. A conexão nova nasce com o cache de catálogo vazio, e o custo é um reconectar a cada 10 minutos por conexão do pool, overhead que na prática não aparece para quem usa a aplicação.
Há uma restrição importante que a Cortex Ridge menciona e que não pode ser ignorada: o PgBouncer só recicla uma conexão quando nenhum cliente está usando ela naquele instante. Isso funciona bem em modo de transaction pooling, onde a conexão fica livre entre uma transação e outra. Em modo session pooling, onde a conexão fica presa ao cliente pela duração de toda a sessão, o server_lifetime tem muito menos chance de agir, porque a conexão raramente fica ociosa.
Como medir o cache de catálogo na própria sessão
Para quem quer verificar esse comportamento antes de mexer em produção, o PostgreSQL expõe a view pg_backend_memory_contexts, disponível a partir da versão 14, que exige superusuário ou um papel com pg_read_all_stats:
SELECT pg_size_pretty(total_bytes) AS catalog_cache
FROM pg_backend_memory_contexts
WHERE name = 'CacheMemoryContext';O teste relatado pela Cortex Ridge, feito no PostgreSQL 18, mostra a escala do efeito: rodar essa query numa sessão nova, consultar cerca de mil tabelas pequenas e rodar de novo fez o contexto de cache saltar de 512 KB para cerca de 8 MB numa única conexão. Os autores ressaltam que tabelas mais largas, com mais índices, consomem mais, e que a query deixa de fora alguns contextos de cache relacionados, então o número real tende a ser maior.
Multiplicar esse crescimento pelo tamanho do pool de conexões, mantidas por 24 horas em vez de minutos, explica por que o servidor inteiro ficava sob pressão de memória.
O resultado e o que fica em aberto
Com o server_lifetime reduzido, o cliente da Cortex Ridge não recebeu mais alertas de memória, e a empresa relata ter conseguido reduzir pela metade a memória da instância do PostgreSQL, o que se traduziu em corte proporcional no custo de infraestrutura em nuvem. O post não detalha os valores exatos de memória antes e depois, embora a legenda da imagem de alertas publicada pela própria Cortex Ridge identifique o provedor de nuvem usado como DigitalOcean; vale tratar o ganho relatado como o resultado de um caso específico, não como promessa automática para qualquer banco.
Essa correção só faz sentido para quem tem a combinação específica descrita aqui: muitos schemas ou tabelas por banco, pooler de conexões de longa duração e modo de pooling que permite reciclar conexões ociosas. Um banco com algumas centenas de tabelas e pool em session mode provavelmente não vai sentir diferença nenhuma ao mexer nesse parâmetro, e vale investigar o plano de execução e os índices antes de qualquer ajuste de infraestrutura, porque memória sob pressão também pode ser sintoma de consulta mal indexada, não só de cache de catálogo.
A Cortex Ridge também documentou, em outro post referenciado no mesmo artigo, como resolver spill para disco usando ALTER FUNCTION ... SET work_mem por função, outro ajuste de memória que vale a leitura para quem administra PostgreSQL em produção com múltiplos workloads concorrentes.
Fonte: Planet PostgreSQL
Highlights
- PgBouncer reduzido de 24h para 10 minutos de server_lifetime cortou memória pela metade no cliente
- 432 schemas e 82.058 tabelas causaram acúmulo de cache de catálogo em conexões de longa duração
- Cache de catálogo PostgreSQL só é liberado ao fechar conexão, nunca automaticamente durante sua vida
- Query pg_backend_memory_contexts mostra crescimento: 512 kB para 8 MB ao consultar 1.000 pequenas tabelas
- Solução aplicável a bancos multi-tenant em transaction pooling; session pooling exige análise caso a caso
Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.
PostgreSQL 14 encerra suporte e obriga plano de migração em produção
Um roteiro de upgrade detalhado pela Stormatics mostra as três decisões que definem se a migração vira problema ou rotina.













