Supabase: porta 5432, 6543 ou conexão direta — qual usar
São três formas de conectar no mesmo banco, com limitações diferentes. Escolher errado gera erros que parecem de código e são de infraestrutura.
O erro tratado aqui
MaxClientsInSessionMode: max clients reached - in Session mode max clients are limited to pool_sizeAmbiente testado
- Supabase Postgres 15
- Supavisor
- Prisma 6.5
- Node 22
Contexto: o que estava rodando
Um projeto no Supabase com três consumidores do mesmo banco: a aplicação Next.js na Vercel, um worker de processamento num contêiner de longa duração, e as migrations rodando no CI.
Todos os três usavam a mesma string de conexão. Cada um quebrou de um jeito diferente.
Os erros
Na aplicação serverless, sob carga:
Error: prepared statement "s0" already existsNo worker, depois de algumas horas:
MaxClientsInSessionMode: max clients reached -
in Session mode max clients are limited to pool_sizeNo CI, ao rodar migration a partir de um runner sem IPv6:
Error: P1001: Can't reach database server at `db.abcdefghijkl.supabase.co:5432`
connect ENETUNREACH 2600:1f1c:4a2:be00:9d21:5b7c:0000:0001:5432Três mensagens sem nada em comum. A causa é a mesma: cada caminho de conexão tem uma limitação, e a string estava errada para dois dos três usos.
Diagnóstico
São três caminhos, não um
O Supabase expõe o mesmo Postgres de três maneiras. A diferença entre elas não é performance — é o que cada uma consegue fazer.

| Conexão direta | Pooler · sessão | Pooler · transação | |
|---|---|---|---|
| Host | db.REF.supabase.co |
aws-0-REGIAO.pooler.supabase.com |
aws-0-REGIAO.pooler.supabase.com |
| Porta | 5432 |
5432 |
6543 |
| Usuário | postgres |
postgres.REF |
postgres.REF |
| Rede | IPv6 (IPv4 é adicional pago) | IPv4 e IPv6 | IPv4 e IPv6 |
| Conexão é sua | Até desconectar | Até desconectar | Só durante a transação |
| Quantos clientes | Poucos | Poucos | Muitos |
| Statements preparados | Sim | Sim | Não (desligue no cliente) |
LISTEN/NOTIFY, advisory lock, set de sessão |
Sim | Sim | Não |
| DDL e migrations | Sim | Sim | Não recomendado |
Note o detalhe fácil de passar batido: o pooler em modo sessão também usa a porta 5432. Só o host e o formato do usuário mudam. Isso faz muita gente achar que está na conexão direta quando está no pooler, e vice-versa.
Por que cada erro aconteceu
prepared statement "s0" already exists — a aplicação
estava no modo transação (porta 6543), correto para serverless, mas sem desligar os statements
preparados no cliente. Em modo transação a conexão é emprestada por transação, e
dois clientes acabam criando s0 na mesma sessão do servidor.
MaxClientsInSessionMode — o worker estava no modo sessão. Nesse modo cada
cliente segura uma conexão real enquanto estiver conectado, e o número de vagas é
pequeno. O pool interno do worker abria mais conexões do que o modo sessão
comporta.
ENETUNREACH — o runner do GitHub Actions não tem IPv6, e
db.REF.supabase.co resolve para um endereço IPv6. Sem o adicional de IPv4, não
há rota. O erro parece “banco fora do ar” e é “rede incompatível”.
A solução
-
Aplicação serverless: modo transação, statements desligados, pool de um.
.env.production (Vercel)DATABASE_URL="postgresql://postgres.REF:SENHA@aws-0-sa-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" DIRECT_URL="postgresql://postgres.REF:SENHA@aws-0-sa-east-1.pooler.supabase.com:5432/postgres"O
connection_limit=1não é economia exagerada: cada invocação atende uma requisição por vez, e um pool maior só cria conexão ociosa que o pooler precisa administrar.prisma/schema.prismadatasource db { provider = "postgresql" url = env("DATABASE_URL") // 6543 — consultas da aplicação directUrl = env("DIRECT_URL") // 5432 — migrations } -
Worker de longa duração: modo sessão, com pool dimensionado.
O erro do worker não era o modo — era o tamanho do pool. Um processo estável pode e deve manter conexões abertas; ele só não pode abrir mais do que existe vaga.
src/worker/db.tsimport postgres from 'postgres'; export const sql = postgres(process.env.SESSION_URL!, { // Um número que cabe no modo sessão, e não o padrão da biblioteca. // Some o pool de todos os processos antes de escolher: duas réplicas // com max: 10 pedem 20 vagas, não 10. max: 5, idle_timeout: 30, connect_timeout: 10, });Antes de escolher o número, veja quantas vagas existem:
show max_connections; select count(*) as em_uso from pg_stat_activity where datname = current_database(); -
CI e migrations: pooler em modo sessão, não a conexão direta.
Este é o ajuste que resolve o
ENETUNREACHsem contratar adicional. O hostpooler.supabase.comresponde em IPv4, e o modo sessão suporta tudo que uma migration precisa..github/workflows/deploy.yml- name: Aplicar migrations env: # Pooler em modo sessão (porta 5432): IPv4 e recursos de sessão completos. DIRECT_URL: ${{ secrets.SUPABASE_SESSION_URL }} run: npx prisma migrate deploy -
Deixar registrado na configuração qual caminho é qual.
O maior risco aqui não é escolher errado uma vez — é alguém copiar a string errada daqui a seis meses. Nomes explícitos previnem isso melhor que documentação:
.env.example# 6543 · modo transação · aplicação serverless # Muitos clientes efêmeros. Sem statements preparados, sem estado de sessão. DATABASE_URL= # 5432 · pooler em modo sessão · migrations, CI e workers # Poucos clientes estáveis. Sessão completa, IPv4 disponível. DIRECT_URL= # db.REF.supabase.co:5432 · conexão direta · só com IPv6 ou adicional IPv4 # Use apenas para manutenção pontual pelo psql. ADMIN_URL=
Como confirmar que resolveu
Confirme por qual caminho cada serviço está entrando:
select
inet_server_addr() as endereco_servidor,
inet_server_port() as porta,
current_user as usuario;
Usuário postgres.REF indica pooler; postgres puro indica conexão direta.
Veja o total de conexões durante o pico. Com a aplicação em modo transação, o número precisa ficar estável mesmo com o tráfego subindo:
select state, count(*)
from pg_stat_activity
where datname = current_database()
group by state;
Force concorrência na aplicação e confirme que o 42P05 não volta:
npx autocannon -c 50 -d 20 https://seu-app.vercel.app/api/produtos
Rode uma migration pelo CI. É o único jeito de validar a rota de IPv4 antes de precisar dela num deploy urgente.
Armadilhas que sobram depois disso
A região do pooler faz diferença. aws-0-us-east-1 a partir de uma função na
região gru1 acrescenta latência de ida e volta em toda consulta. Alinhe a
região do projeto com a região onde a aplicação roda.
O painel oferece a string pronta — leia qual delas você copiou. As três ficam no mesmo lugar da interface, com rótulos parecidos. É o erro de origem mais comum.
O usuário da conexão decide o que você enxerga. O pooler conecta como
postgres.REF, e a conexão direta como postgres — que é dono das tabelas e
ignora as políticas de Row Level Security. Trocar de caminho pode fazer
uma consulta passar a devolver linhas que antes estavam corretamente ocultas.
Modo transação não faz mágica com transação longa. Um $transaction que faz
chamada HTTP no meio segura a conexão do pooler durante toda a chamada externa,
anulando o benefício. Rede fora de transação, sempre.
connection_limit=1 derruba consultas paralelas na mesma requisição. Se uma
rota dispara três consultas com Promise.all, elas passam a ser sequenciais. Em
geral é aceitável; quando não for, suba para 2 ou 3 — nunca para o padrão.
Continue por aqui
Dados & Migrações
Prisma em serverless: resolvendo o timeout do connection pool
O pool do Prisma estoura quando cada função serverless abre o próprio conjunto de conexões. O diagnóstico completo e as três correções que aplicamos.
Dados & Migrações
Prisma, Drizzle ou postgres.js: medindo o que importa em serverless
A maioria dos benchmarks de ORM mede a coisa errada. O que pesa em função efêmera é a primeira query, não a milésima — e o script para medir isso.
Dados & Migrações
RLS no Supabase: por que sua query retorna array vazio
Com Row Level Security ligada, o SELECT bloqueado não dá erro — devolve nada. Como depurar a política sem desligar a segurança para "testar".