Connection pooling no Postgres para cargas serverless

Funções serverless não mantêm conexões com banco de dados — abrem, executam uma query e descartam. Connection pooling no Postgres para serverless multiplexa milhares de clientes efêmeros sobre um pool upstream pequeno, ou o banco atinge "too many connections" com tráfego normal.

Engenharia8 min de leitura
PostgresConnection poolingServerlessSupabaseBanco de dados
Compartilhar

Uma função na Vercel que abre conexão direta com Postgres a cada cold start não tem problema de conexão no primeiro request. Tem na quinquagésima invocação concorrente, quando Postgres retorna FATAL: sorry, too many clients already e metade dos requests trava até timeout. Runtimes serverless escalam instâncias na horizontal. Postgres escala conexões na vertical — e cada conexão faz fork de um processo backend consumindo cerca de 5–10 MB de RAM. Connection pooling no Postgres para serverless não é camada de otimização. É o mecanismo que permite funções sem estado conversarem com banco com estado sem esgotar o teto de conexões em pico de tráfego que seria normal para a camada de aplicação.

A falha é fácil de não ver em desenvolvimento. Testes locais rodam uma instância de função. Staging roda dez. Produção roda quinhentos cold starts durante deploy, cada um reivindicando slot que Postgres mantém até o cliente fechar ou idle timeout vencer — muitas vezes mais que a vida da função se o driver faz pool errado ou connection string aponta para host direto.

Conexões diretas falham em silêncio até falhar em catástrofe

Postgres aloca um processo servidor por conexão de cliente. Em instâncias managed com limite de 60–200 conexões conforme tier de compute, a conta é implacável. Duzentas invocações serverless concorrentes abrindo conexão direta consomem todo o orçamento. Workers em background, ferramentas de migração, agentes de monitoramento e o pooler também precisam de slots. A aplicação não recebe 200 — recebe o que sobra depois que o resto do fleet reivindica sua parte.

A sequência de sintomas é previsível. Latência p99 de queries sobe primeiro — conexões enfileiram na camada accept do Postgres. Depois sobem taxas de erro — conexões novas rejeitadas. Por fim serviços não relacionados falham porque banco compartilhado esgotou slots. Dashboards mostram erros de aplicação. Logs de banco mostram violações do limite de conexões. A correção não é só "escalar compute" — instâncias maiores elevam o teto mas não removem descompasso entre clientes efêmeros e processos servidor de longa vida.

Funções serverless esgotam conexões; não as sustentam com responsabilidade.

Backends de longa vida — API Node.js em container, servidor Rails, worker em background — podem manter pool in-process pequeno de cinco a vinte conexões amortizado em milhares de requests. Serverless quebra essa suposição. Cada invocação é cliente potencialmente novo. Autoscaling multiplica clientes mais rápido do que Postgres consegue fazer fork de processos. Pooling deve ocorrer fora da função — em proxy que multiplexa muitas sessões cliente curtas sobre poucas conexões upstream.

Modo transação é o default para pooling serverless

Poolers de conexão operam em dois modos que importam para decisões de arquitetura.

Modo transação atribui conexão upstream do Postgres a um cliente só durante uma transação. No commit ou rollback, conexão volta ao pool. Próximo cliente — possivelmente outra invocação de função — a recebe. Isso encaixa com semântica serverless: abrir, query, commit, fechar. Milhares de instâncias de função podem compartilhar dez a cinquenta conexões upstream se cada query roda dentro de limite de transação explícito.

Modo sessão mantém conexão upstream durante toda sessão do cliente. Necessário para prepared statements que persistem entre queries, advisory locks, comandos SET, tabelas temporárias e LISTEN/NOTIFY. Funções serverless que precisam de estado com escopo de sessão não deveriam rodar em pools de modo transação sem redesign.

ModoTempo de hold upstreamEncaixe serverlessFeatures de sessão
TransaçãoUma transaçãoDefault para edge e funçõesSem prepared statements entre chamadas
SessãoToda conexão clienteSó backends persistentesPrepared statements, locks, temp tables

No Supabase, pooler compartilhado Supavisor expõe modo transação na porta 6543 e modo sessão na porta 5432 via hostname do pooler. Conexões diretas usam db.[project].supabase.co:5432 para migrações, pg_dump e tarefas admin — não para código de aplicação em runtimes serverless.

Planos pagos também oferecem PgBouncer dedicado co-localizado com instância de banco — só modo transação, menor latência que pooler multi-tenant compartilhado, alcançável na porta 6543 via host direto de banco. Trade-off é operacional: Supavisor compartilhado adiciona ~1–2 ms por query mas isola carga do pool do CPU do banco. PgBouncer dedicado é mais rápido por query mas compartilha compute com Postgres.

Connection string serverless para Postgres tem quatro requisitos

Connection strings mal configurados causam mais incidentes de produção que índices faltando. Quatro ajustes definem comportamento correto de connection pooling no Postgres para serverless.

Apontar para pooler, não host direto. Código de aplicação na Vercel, Cloudflare Workers, AWS Lambda e similares deve usar URL do pooler em modo transação. Migrações e scripts admin one-off usam conexão direta.

Definir connection_limit=1 por instância de função. Runtimes serverless não se beneficiam de pools in-process de dez conexões — cada instância trata um request por vez em configurações típicas. Múltiplas conexões por instância multiplicam consumo de slots sem ganho de throughput.

Desabilitar prepared statements em modo transação. PgBouncer e Supavisor em modo transação não garantem mesma conexão upstream entre queries. ORMs que preparam por default — Prisma é caso comum — precisam ?pgbouncer=true ou flags equivalentes do driver para evitar erros prepared statement already exists.

Manter transações curtas. Transações longas retêm conexões upstream e bloqueiam pool. Função serverless que abre transação, chama API externa e depois faz commit retém slot durante espera de rede. Padrão: ler, computar, escrever — I/O externo fora de limites de transação.

# Supabase modo transação (tráfego de aplicação serverless)
DATABASE_URL="postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true"
 
# Conexão direta (só migrações — nunca de funções serverless)
DIRECT_URL="postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres"
import { Pool } from "pg"
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 1,
  idleTimeoutMillis: 10_000,
  connectionTimeoutMillis: 5_000,
})
 
export async function getUser(id: string) {
  const client = await pool.connect()
  try {
    await client.query("BEGIN")
    const result = await client.query(
      "SELECT id, email FROM profiles WHERE id = $1",
      [id]
    )
    await client.query("COMMIT")
    return result.rows[0]
  } catch (err) {
    await client.query("ROLLBACK")
    throw err
  } finally {
    client.release()
  }
}

Políticas Row-Level Security ainda avaliam por query em conexões pooled — JWT ou papel de sessão deve ser definido dentro da mesma transação. No Supabase, cliente JavaScript via PostgREST evita conexões Postgres diretas; pooling importa para código server-side que usa driver de banco diretamente. Equipes que otimizam desempenho de RLS em escala compõem latência do pooler com custo de política por linha — outra razão para transações curtas e indexadas.

Quando pooling custa mais do que ajuda

Pooling não é grátis. Proxy adiciona latência de hop — tipicamente 1–2 ms por query no Supavisor, menos no PgBouncer co-localizado. Para query que roda em 0,5 ms, esse overhead importa. Para uma que roda em 50 ms, não.

Pular pooler quando:

  • Um único processo de longa vida serve todo tráfego com pool in-process dimensionado à concorrência real. Servidor Node.js dedicado em VM com max: 20 não precisa pooler externo salvo aproximar-se de limites de conexão entre múltiplas instâncias.
  • Cliente usa PostgREST ou API HTTP do Supabase. Esses caminhos não abrem conexões Postgres cruas do código de aplicação. Pooling é problema da plataforma.
  • Features com escopo de sessão são necessárias — caches de prepared statements entre requests, advisory locks para coordenação de jobs, COPY em contexto de sessão. Rodar estes em endpoints de pooler em modo sessão ou conexões diretas de workers persistentes, não de caminhos serverless em modo transação.

Usar pooler quando:

  • Runtimes serverless ou autoscaling abrem conexões por invocação.
  • IPv4 é necessário e host direto é só IPv6 — pooler compartilhado do Supabase fornece IPv4 em todos os tiers.
  • Contagem de conexões se aproxima de limites de instância em load tests que refletem invocações concorrentes, não curls sequenciais.

Como funciona connection pooling no Postgres para serverless?

Estas perguntas cobrem decisões que equipes erram ao passar de desenvolvimento local para deploys serverless em produção.

Funções serverless devem usar modo transação ou modo sessão?

Modo transação é default para funções serverless. Cada invocação deve abrir conexão, executar uma ou mais sentenças dentro de transação, fazer commit e liberar. Modo sessão é para backends persistentes que precisam prepared statements, advisory locks ou persistência de SET entre queries. Apontar código de aplicação serverless para porta 6543 (modo transação no Supabase). Apontar ferramentas de migração e workers de longa vida para conexões diretas ou endpoints em modo sessão.

Por que Prisma falha com "prepared statement already exists" atrás de pooler?

Poolers em modo transação reatribuem conexões upstream entre clientes. Prisma prepara statements por default e espera que mesma conexão persista. Próxima query pode cair em outra conexão upstream onde nome do prepared statement colide. Adicionar ?pgbouncer=true à URL para Prisma usar prepared statements sem nome compatíveis com transaction pooling.

Cliente JavaScript do Supabase precisa de connection pooling?

Não — para uso típico frontend e serverless via supabase-js, queries vão por PostgREST sobre HTTP, não conexões Postgres diretas. Pooling importa para código server-side com pg, Drizzle com driver direto, Prisma ou runtimes edge que conectam a Postgres nativamente. Se único acesso a banco é via SDK do Supabase, configuração de pooling é irrelevante para código de aplicação.

Um ponto de vista oposto

Argumento frequente sustenta que funções serverless não deveriam conectar a Postgres diretamente — que todo acesso a banco deveria fluir por camada API, gateway de edge functions ou PostgREST, tornando pooling em nível de driver anti-padrão herdado de arquiteturas serverful.

Esse argumento é correto para muitas arquiteturas de produto. É incompleto para server-side rendering, jobs em background co-localizados em runtimes serverless, migrações de código legacy com ORM pesado e edge databases que expõem wire protocol do Postgres. Quando conexões diretas existem, pooling é obrigatório — alternativa não é "usar HTTP em vez disso" mas "esgotar conexões e falhar sob carga".

Divisão produtiva: HTTP/API para acesso a dados client-facing onde possível; conexões diretas pooled para código server-side que genuinamente precisa acesso wire-protocol, com modo transação e max: 1 por instância.

O que vale a pena lembrar

  • Connection pooling no Postgres para serverless é obrigatório quando funções abrem conexões wire-protocol — conexões diretas esgotam slots antes de queries falharem visivelmente.
  • Modo transação multiplexa muitos clientes efêmeros sobre poucas conexões upstream; modo sessão é para backends persistentes com features de sessão.
  • Supabase: porta 6543 para tráfego de aplicação serverless, host direto para migrações, pgbouncer=true para Prisma.
  • Definir max: 1 (ou connection_limit=1) por instância serverless — pools in-process multiplicam consumo de slots sem benefício.
  • Manter transações curtas; I/O externo fora de limites de transação libera conexões upstream mais rápido.
  • Pooling adiciona ~1–2 ms por query — pular para servidores únicos de longa vida com acesso só PostgREST.

Conclusão

Emparelhamento serverless mais Postgres falha na camada de conexão, não na de query. Aplicações que funcionam localmente batem em limites de produção porque autoscaling multiplica clientes que Postgres nunca foi desenhado para aceitar um a um. Pooling traduz demanda efêmera em pegada upstream limitada — modo transação, uma conexão por instância, transações curtas, connection string correta.

Auditoria de configuração é curta: rastrear cada runtime que abre conexão com driver Postgres, verificar que aponta para pooler em modo transação, confirmar prepared statements desabilitados onde necessário, e load-test com invocações concorrentes em vez de requests sequenciais. Todo o resto — índices, políticas RLS, planos de query — é irrelevante se conexão nunca se estabelece. Corrigir pool primeiro.

Artigos relacionados

Paleta de comandos

Pesquise um comando para executar...