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.
O erro tratado aqui
Timed out fetching a new connection from the connection pool. (Current connection pool timeout: 10, connection limit: 9)Ambiente testado
- Next.js 15.3
- Prisma 6.5
- Supabase Postgres 15
- Vercel · Node 22
Contexto: o que estava rodando
Uma aplicação Next.js no App Router, hospedada na Vercel, com Prisma falando com um Postgres do Supabase. Nada exótico: rotas de API lendo e escrevendo pedidos, uma página de listagem com paginação, e um webhook de pagamento que grava o status da transação.
Em desenvolvimento, tudo passava. Em produção com trânsito baixo, também. O problema apareceu no primeiro pico real — uma campanha que trouxe algumas centenas de acessos simultâneos. A partir de um certo volume, uma fatia das requisições começou a devolver 500, enquanto outra fatia continuava respondendo normalmente. Esse detalhe — parte falhando — é a primeira pista, e vou voltar nela.
O erro
PrismaClientKnownRequestError:
Invalid `prisma.order.findMany()` invocation:
Timed out fetching a new connection from the connection pool.
More info: http://pris.ly/d/connection-pool
(Current connection pool timeout: 10, connection limit: 9)
at Ai.handleRequestError (/var/task/node_modules/@prisma/client/runtime/library.js:121:7749)
at Ai.handleAndLogRequestError (/var/task/node_modules/@prisma/client/runtime/library.js:121:7115)
at Ai.request (/var/task/node_modules/@prisma/client/runtime/library.js:121:6799)
code: 'P2024'E, intercalado com ele, um segundo erro — diferente, vindo do banco e não do Prisma:
FATAL: remaining connection slots are reserved for non-replication superuser connections
FATAL: sorry, too many clients alreadySão dois erros distintos, com causas encadeadas. Tratá-los como se fossem o mesmo é o que faz a maioria das tentativas de correção falhar.
Diagnóstico
O connection limit: 9 não é o limite do banco
A primeira leitura errada que fiz foi assumir que connection limit: 9 era uma
configuração minha. Não é. Quando você não define nada, o Prisma calcula o
tamanho do pool assim:
connection_limit = num_physical_cpus × 2 + 1
O runtime da Vercel reportava 4 vCPUs, daí o 9. Esse número descreve o pool interno do Prisma dentro daquela instância, não a capacidade do Postgres.
Cada invocação concorrente tem o próprio pool
Aqui está o ponto que muda tudo. Em serverless não existe “o servidor”. Existem
N instâncias da função, criadas sob demanda, e cada instância que executa
new PrismaClient() carrega o próprio pool de 9 conexões.
Com 50 invocações concorrentes, a conta é:
50 instâncias × 9 conexões = 450 conexões abertas contra o Postgres
Um projeto Supabase no plano inicial trabalha com algo na casa de 60 conexões
diretas disponíveis, já descontando as reservadas para superusuário. O banco
recusa o excedente — daí o too many clients already. E as instâncias que não
conseguiram abrir conexão ficam esperando no próprio pool até estourar o
timeout de 10 segundos — daí o P2024.

O terceiro suspeito: a query que segura a conexão
Vale medir antes de mexer na infraestrutura. Uma conexão só volta ao pool quando a query termina. Uma query de 800 ms segura a conexão por 800 ms; a mesma query com o índice certo devolve em 12 ms e libera o slot 60 vezes mais rápido. Pool pequeno com query rápida costuma sustentar muito mais carga do que pool grande com query lenta.
Foi o que rodei no SQL Editor do Supabase para confirmar quem estava segurando o quê:
-- Quantas conexões existem agora, e em que estado
select state, count(*)
from pg_stat_activity
where datname = current_database()
group by state
order by count desc;
-- As consultas que mais consomem tempo total no banco
select
calls,
round(mean_exec_time::numeric, 2) as media_ms,
round(total_exec_time::numeric, 2) as total_ms,
query
from pg_stat_statements
order by total_exec_time desc
limit 10;
O resultado foi revelador: a maior parte das conexões estava em idle. Não era
o banco trabalhando demais — era conexão parada ocupando vaga.
A solução
-
Apontar a aplicação para o pooler, não para o banco direto.
O Supabase expõe duas portas. A
5432é conexão direta com o Postgres. A6543é o Supavisor em modo transaction (a diferença entre as três formas de conectar merece atenção própria): ele mantém um punhado de conexões reais com o banco e as empresta por transação para centenas de clientes.Serverless precisa da
6543. É exatamente o cenário para o qual o pooler existe: muitos clientes efêmeros, cada um usando a conexão por milissegundos..env.production# Aplicação — via pooler, modo transaction DATABASE_URL="postgresql://postgres.SEU_REF:SENHA@aws-0-sa-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" # Migrations — conexão direta, sem pooler DIRECT_URL="postgresql://postgres.SEU_REF:SENHA@aws-0-sa-east-1.pooler.supabase.com:5432/postgres" -
Baixar
connection_limitpara 1.Parece contraintuitivo, mas é a configuração correta atrás de um pooler em modo transaction. Cada invocação atende uma requisição por vez; um pool de 9 por instância só serve para multiplicar conexões ociosas. Com
1, aquelas 450 conexões do exemplo viram 50 — e o Supavisor as multiplexa em um número bem menor de conexões reais.O
pgbouncer=truena mesma URL é obrigatório: ele desliga os prepared statements do Prisma. Sem isso você troca este erro por outro,prepared statement "s0" already exists, porque o pooler devolve conexões diferentes a cada transação. -
Separar a URL de migrations no schema.
Migrations precisam de conexão direta — elas usam recursos de sessão que o modo transaction não suporta.
prisma/schema.prismadatasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_URL") } -
Garantir uma única instância do PrismaClient.
Em produção o problema é o número de instâncias da função. Em desenvolvimento é o hot reload, que recria o módulo e vaza um
PrismaClientnovo a cada alteração de arquivo, até o banco local recusar conexões.src/lib/prisma.tsimport { PrismaClient } from '@prisma/client'; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'], }); // Em produção o módulo é congelado no bundle; guardar no global só importa // no dev, onde o hot reload reavalia este arquivo a cada salvamento. if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma; }Todo acesso ao banco passa a importar deste arquivo. Um
new PrismaClient()perdido em qualquer rota anula as três etapas anteriores.
Como confirmar que resolveu
Não confie no “parou de dar erro”. Três verificações objetivas:
1. A contagem de conexões diretas caiu. Rodando o pg_stat_activity durante
carga equivalente, o total tem que ficar estável e baixo, em vez de subir junto
com o tráfego.
2. As migrations continuam rodando. Este é o teste que gente esquece — a
DIRECT_URL errada só aparece no próximo deploy com migration:
Applying migration `20260822_add_order_status_index`
The following migration have been applied:
migrations/
└─ 20260822_add_order_status_index/
└─ migration.sql
All migrations have been successfully applied.3. O erro P2024 sumiu dos logs sob carga. Vale gerar carga de propósito, em
vez de esperar o próximo pico orgânico:
npx autocannon -c 100 -d 30 https://seu-app.vercel.app/api/orders
Armadilhas que sobram depois disso
Prepared statements. Se aparecer prepared statement "s0" already exists,
o motivo está em como o pooler empresta conexões — falta o
pgbouncer=true na DATABASE_URL — ou algum ponto do código está
abrindo conexão com uma URL diferente.
Transações longas. No modo transaction, a conexão fica presa ao cliente
durante toda a transação. Um prisma.$transaction() que faz chamada HTTP no
meio segura um slot do pooler pelo tempo da chamada externa. Chamada de rede não
entra dentro de transação.
Advisory locks e LISTEN/NOTIFY. Dependem de estado de sessão e não
funcionam pela porta 6543. Se você usa esses recursos, aquele caminho
específico precisa da conexão direta.
O índice ainda é o que mais rende. Ajustar o pool contorna o sintoma. Uma
query de listagem sem índice em status continuará sendo o gargalo no próximo
pico — só que agora com o pool bem configurado. Diagnóstico de query lenta com
EXPLAIN ANALYZE é assunto de outro artigo, e é para lá que
eu iria em seguida.
Continue por aqui
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".
Dados & Migrações
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.