Prisma + PostgreSQL em serverless: usar PgBouncer e padrões de pooling
Por John Pires · 08 de março de 2026

Contexto e motivacao
Por que isso importa em ambientes serverless
Serverless altera o modelo de custo e execução: funções curtas, instâncias efêmeras e alta concorrência por usuário. Em poucas palavras, cada invocação pode acabar tentando abrir uma conexão direta com o banco. No PostgreSQL, conexões são relativamente caras em memória e CPU. Quando o tráfego cresce, o número de conexões pode explodir, levando o banco a ficar sem recursos e a recusar novas conexões — resultado: erros 500 em produção.
O problema típico que vejo em code review
Recebo muito código onde o autor instancia um PrismaClient dentro do handler de cada requisição. Isso funciona localmente e em low-traffic, mas em serverless (Vercel, Netlify, AWS Lambda) é uma bomba relógio. O padrão é simples e equivocado: cada instância de função cria sua própria conexão ao Postgres. Em escala, isso quebra.
Antes de partir para soluções, mantenha isso em mente: o objetivo é reduzir conexões ao banco e controlar a multiplexação delas. PgBouncer entra como um intermediário leve que converte muitos clientes em menos conexões backend.
Como funciona por baixo dos panos
O que é PgBouncer e o que ele faz
PgBouncer é um pooler de conexão para PostgreSQL escrito em C. Ele aceita conexões de clientes e as multiplica em um número controlado de conexões reais ao servidor Postgres. O ganho é óbvio: em vez de 1000 clientes ocupando 1000 conexões no Postgres, você pode ter 1000 clientes atendidos por, por exemplo, 50 conexões backend.
Pool modes e implicações
Existem três modos principais em PgBouncer: session, transaction e statement. Cada um tem trade-offs:
- session: o client fica ligado à mesma conexão backend durante toda a sessão. Seguro para qualquer feature do Postgres (temp tables, prepared statements, session variables), mas consome conexões backend como se não houvesse pooler.
- transaction: o client pega uma conexão backend apenas durante a execução da transação; entre transações a conexão é devolvida ao pool. É o modo recomendado para ganhar maior multiplexação com aplicações que não dependem de features de sessão.
- statement: ainda mais agressivo. Aloca uma conexão backend apenas para cada comando SQL. Risco alto de incompatibilidade com transações.
Para serverless, transaction costuma ser o melhor compromisso, desde que a aplicação não use recursos dependentes de sessão.
Tip: escolha transaction mode para maximizar multiplexação — mas valide que seu ORM/driver não usa recursos de sessão que quebram nesse modo.
Implementacao passo a passo
O jeito errado (exemplo real)
Trecho típico que vejo em PRs: instanciar PrismaClient dentro do handler de cada invocação. Isso cria conexões repetidas.
// O que faz: cria um novo PrismaClient a cada requisição (ruim em serverless)
import { PrismaClient } from '@prisma/client'
export default async function handler(req, res) {
const prisma = new PrismaClient() // errado: instancia por requisição
const users = await prisma.user.findMany()
await prisma.$disconnect()
res.json(users)
}
O que acontece: em baixa carga funciona. Em alta concorrência, você passa a abrir e fechar centenas de conexões por segundo. Dependendo de warm starts e cold starts do provider, isso sobrecarrega o Postgres.
O jeito certo: singleton e reuse
Reutilize uma instância global de PrismaClient. Em runtime serverless com hot reloading (ex: Next.js, Vercel) você deve controlar múltiplas avaliações do módulo.
// O que faz: cria um singleton global para reusar PrismaClient entre invocações
import { PrismaClient } from '@prisma/client'
const globalWithPrisma = globalThis as unknown as { prisma?: PrismaClient }
const prisma = globalWithPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalWithPrisma.prisma = prisma
export default async function handler(req, res) {
const users = await prisma.user.findMany()
res.json(users)
}
O que acontece: mantém conexões e pools do driver reutilizáveis entre invocações quentes, reduzindo a taxa de criação de conexões. Ainda assim, isso por si só não resolve pico de concorrência extremo; é aqui que PgBouncer entra.
Configuração básica do PgBouncer (docker-compose)
Exemplo funcional: rodar Postgres + PgBouncer localmente para validar. Abaixo há um docker-compose simples. Contexto: você testa localmente, valida pool_mode, connection_limit e authentication.
# O que faz: docker-compose com postgres e pgbouncer para testes locais
version: '3.8'
services:
db:
image: postgres:14
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- '5432:5432'
pgbouncer:
image: edoburu/pgbouncer
environment:
DB_HOST: db
DB_PORT: 5432
DB_USER: test
DB_PASSWORD: test
ports:
- '6432:6432'
O que acontece: expõe PgBouncer na porta 6432. Aponte sua string de conexão do Prisma para esse host/porta para testar o comportamento do pooler.
Patterns e boas praticas
Arquitetura recomendada
Combinar três camadas: cliente (serverless), PgBouncer (pooler) e Postgres (backend). No client, mantenha um singleton do PrismaClient e configure timeouts adequados. No PgBouncer, use transaction pool_mode com limites prudentes e health checks.
Configurações de PgBouncer que uso em produção
Config minimal com justificativa:
- pool_mode = transaction — melhora multiplexação
- max_client_conn = 1000 — número de clientes que o pooler aceita
- default_pool_size = 50 — número máximo de conexões backend por database/user
- reserve_pool_size = 10 e reserve_pool_timeout = 5 — buffer para spikes curtos
- server_lifetime/server_idle_timeout — para reciclar conexões e evitar memory leak do Postgres
# O que faz: exemplo de pgbouncer.ini minimal para produção
[databases]
testdb = host=postgres port=5432 dbname=testdb
[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 50
reserve_pool_size = 10
reserve_pool_timeout = 5
server_lifetime = 3600
server_idle_timeout = 300
log_connections = 1
log_disconnections = 1
O que acontece: com essa configuração você reduz conexões ao Postgres e ganha prevenção contra picos, mantendo estabilidade durante aumentos repentinos de tráfego.
Armadilhas comuns e como evitar
Armadilha: features de sessão incompatíveis com transaction pooling
Muitas falhas em produção acontecem porque a aplicação usa recursos que assumem a mesma conexão backend por toda a sessão:
- temporary tables
- prepared statements nomeados
- SET LOCAL / session variables
- advisory locks
Quando PgBouncer está em transaction mode, essas features quebram porque uma transação pode rodar em uma conexão e a próxima em outra. Resultado: PreparedStatement não encontrado, variáveis inesperadas ou perda de lock.
Como detectar e mitigar
Detecção: logs de erro do app mostrando mensagens como "prepared statement does not exist" ou comportamento inconsistente de dados temporários. Mitigação:
- Identifique código que cria temp tables ou usa session-level SET; refatore para evitar essas operações.
- Se não for possível remover, rode PgBouncer em session mode para esses workloads específicos — aceitando reduzir multiplexação.
- Considere usar Prisma Data Proxy ou um pooler gerenciado como alternativa quando a aplicação não pode ser refatorada.
Nota: em muitas aplicações web CRUD típicas, você pode operar sem features de sessão e aproveitar transaction pooling sem problemas.
Case real de erro: temp table em job paralelo
Cenário: job cria uma temp table, faz cálculos e insere resultados numa tabela permanente. Em transaction pooling, a temp table desaparece entre queries porque a conexão backend troca. O job falha ou produz resultados incorretos.
Solução: reimplemente o job para usar tabelas permanentes com um identificador de execução ou execute o job em um worker que usa session pooling dedicado.
Mau uso de transações no app
Evite abrir uma transação e mantê-la viva entre operações assíncronas longas (ex: I/O externo). Em serverless, isso prende conexões backend por mais tempo e reduz a eficiência do pool. Transações devem ser curtas e confinadas ao mínimo necessário para garantir atomicidade.
Checklist de revisão de código (pense como reviewer)
Sintomas de código problemático
Itens que disparam preocupação durante code review:
- PrismaClient instanciado dentro de handlers
- Uso de temp tables em requests web
- SET SESSION ou SET LOCAL sendo aplicado em endpoints
- Transações longas com awaits externos
Recomendações práticas para PRs
Ao aprovar PRs, exija:
- Singleton global do PrismaClient
- Validação que não há uso de features de sessão, ou justificação para session pooling
- Testes de carga básicos contra PgBouncer local (docker-compose) mostrando comportamento ok
Conclusao pratica
Resumo das decisões técnicas
Em serverless, controle de conexões é crucial. Use singleton do PrismaClient para reduzir criações desnecessárias. Empregue PgBouncer para multiplexação. Prefira transaction pool_mode quando possível, mas valide que sua aplicação e ORM não dependem de recursos de sessão.
Passos imediatos para aplicar hoje
1) Substitua novas instâncias de PrismaClient por um singleton global. 2) Coloque um PgBouncer diante do seu banco (teste local via Docker). 3) Rode testes de carga e procure erros relacionados a prepared statements ou temp tables. 4) Se aparecerem, escolha entre refatorar o uso ou trocar o pool_mode para session apenas para esses workflows.
Aplicando esses padrões você reduz risco de downtime, controla custos e melhora a previsibilidade operacional. Em code reviews, priorize segurança da conexão e brevidade das transações. PgBouncer não é uma bala de prata: é uma ferramenta potente se usada com consciência dos trade-offs.
Leve para a prática: implemente o singleton, configure PgBouncer em transaction mode e execute um teste de stress simples com 200-500 clientes concorrentes. Observe métricas de conexões backend e latência. Ajuste default_pool_size e reserve_pool_size conforme necessidade.
Se precisar, avalie Prisma Data Proxy como alternativa gerenciada que abstrai parte da complexidade de pooling para ambientes serverless.
Assuntos: PostgreSQL, Prisma, pgbouncer, serverless, pooling, Performance
Leia também
Transações no PostgreSQL com Prisma: commit, rollback e locks passo a passo
Guia prático e passo a passo de transacoes no Postgres com Prisma: como usar commit, rollback e locks, exemplos com transferencia entre contas, SELECT FOR…
UUID vs SERIAL vs ULID no PostgreSQL: escolher e usar com Prisma
Comparativo prático entre UUID, SERIAL e ULID em PostgreSQL com exemplos e mapeamentos no Prisma. Guia passo a passo, comandos, scripts Node.js e dicas…
Indexando e consultando JSONB no PostgreSQL com GIN + pg_trgm e Prisma
Como indexar e consultar JSONB no PostgreSQL: combine GIN jsonb_path_ops para contenção com GIN+pg_trgm em expressões textuais, e integre via migrations SQL…
Otimização de consultas com Prisma e PostgreSQL para reduzir latência e custos em 2026
Técnicas avançadas para reduzir latência e custos em aplicações com Prisma e PostgreSQL: projeção de campos, batching, cursor pagination, índices, pooling e…
Migrações zero-downtime no PostgreSQL com Prisma: blue-green e backfill
Guia prático para executar migrações zero-downtime em PostgreSQL com Prisma. Cobertura de padrões expand-fill-contract, blue-green deploy, backfill em…
Interpretando EXPLAIN ANALYZE no PostgreSQL: guia prático para iniciantes
Guia prático para iniciantes sobre EXPLAIN ANALYZE no PostgreSQL: aprenda a rodar, interpretar planos, identificar problemas (seq scan, nested loop) e aplicar…