你的 Vercel / Cloudflare / Netlify 函数突然全线 500,日志里满是:
error: remaining connection slots are reserved for non-replication
superuser and rds_superuser connections
或者:
sorry, too many clients already
这是 Supabase(其实是 Postgres)经典的连接池耗尽。你的 Free 档默认 60 连接、Pro 档 200 连接,听起来很多,但 Serverless 平台一开 100 个并发实例,每个开一个 connection 就直接爆。问题不在 connection 数太少,而在 Serverless + 直连 Postgres 本身是反模式。
理解关键:Postgres 的连接是重资源(每个 ~10MB 内存),不是 HTTP 连接那种轻便资源。Serverless 大规模并发必须经过 connection pooler(Supabase 内置 Supavisor / PgBouncer)。
常见原因
按命中率从高到低:
1. Serverless 直连 :5432(不走 pooler)
最高频。代码用 Supabase Dashboard 给的”Direct connection” string,每个 Lambda / Workers 实例独立开 connection。突发流量 → 100 个实例 → 100 个 connection → 爆。
如何判断:connection string 是 postgres://...supabase.com:5432/postgres 还是 :6543?5432 = 直连,6543 = pooler。
2. 没释放连接 / 长事务
const client = await pool.connect();
const result = await client.query('SELECT ...');
// ❌ 忘了 client.release()
每个 request 漏 1 个连接,几小时就耗尽。
如何判断:日志里 connection count 单调递增不下降。
3. pgbouncer mode 选了 session
Supavisor 默认 transaction mode,但有些教程让你切到 session mode(兼容性更好但占连接)。session mode 失去 pooling 优势。
如何判断:connection string 里 ?pgbouncer=true&pool_mode=session?
4. 用了 PgBouncer 不支持的 statement
prepared statements、LISTEN、temporary tables 等在 transaction mode pooler 下不工作;ORM(如 Prisma)默认会用 prepared statements 必须特殊配置。
如何判断:报 prepared statement "X" does not exist 或类似。
5. 后台 cron 跑慢查询占连接
定时统计查询跑 30 秒、同时正常 traffic 进来也要 connection → 池子被 cron 占了。
如何判断:错误集中在 cron 触发时段。
6. 旁路服务(pgAdmin、Metabase)开了大量连接
你 / 同事用 GUI 工具直连 prod DB,每个 GUI 占 5-10 个连接。
如何判断:DB 上 SELECT * FROM pg_stat_activity 看 application_name。
最短修复路径
Step 1:紧急止血——kill 老连接
-- 在 Supabase Dashboard SQL Editor 跑
SELECT pid, usename, application_name, state, query_start
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
-- kill 老于 5 分钟的 idle in transaction
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND query_start < now() - interval '5 minutes';
立刻让 connection 空出来恢复服务,再慢慢修。
Step 2:换 pooler 连接串
Supabase Dashboard → Project Settings → Database → Connection string:
直连(避免在 Serverless 用):
postgres://postgres:[password]@db.xyz.supabase.co:5432/postgres
Supavisor pooler(Serverless 必用):
postgres://postgres.xyz:[password]@aws-0-region.pooler.supabase.com:6543/postgres
把代码 / env var 改成 pooler 字符串。重启服务。
Step 3:确认 pool_mode
postgres://...@...:6543/postgres?pgbouncer=true
^^^^^^^^^^^^
不指定 = transaction(推荐)
如果你显式加了 &pool_mode=session,去掉。session mode 失去 pooling 意义。
Step 4:Prisma / ORM 特殊处理
// .env
DATABASE_URL="postgres://...@:6543/postgres?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgres://...@:5432/postgres" // migration 用
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL") // prisma migrate 走 direct
}
Prisma + pgbouncer 必加 ?pgbouncer=true&connection_limit=1。
Step 5:query 必 release
// ❌ 漏 release
const client = await pool.connect();
await client.query(...);
// 缺 finally
// ✅ 正确
const client = await pool.connect();
try {
await client.query(...);
} finally {
client.release();
}
// 或更简单:用 pool.query 一次性,自动 release
await pool.query('SELECT * FROM users WHERE id = $1', [id]);
Step 6:监控 + 警报
-- 当前 active connections
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
-- 长 idle in transaction(漏 release 信号)
SELECT pid, query, query_start, state
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND query_start < now() - interval '1 minute';
设置 alert:connection count > 80% cap 立刻通知。
Step 7:升级 plan / connection limit
Pro plan 默认 200 connections,Team 400,Enterprise 可调。短期不够先升 plan,长期还是要优化 pooling。
预防建议
- Serverless 项目永远走
:6543pooler,不要碰:5432 - 长跑后端服务(Fly.io、Railway、自建 EC2)才用 :5432 直连
- 用 ORM 时配
connection_limit=1(serverless 每实例只要 1 个连接) - 每个 query 必 await + try/finally release
- 监控 connection count,超 80% cap alert
- 定时任务(cron、batch)用独立 connection / 独立 service,不抢主 pool
- 禁止从笔记本直连 prod DB——用 Supabase Studio 或加 read-replica
- 大流量项目从 day 1 就估算 connection 需求,免费档 60 连接很容易爆