你的 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)经典的连接池耗尽。
最快修复: 把 serverless 代码指向 6543 端口的 transaction 模式 pooler(...pooler.supabase.com:6543),而不是 :5432 直连串。再确保每个 query 都正确释放连接。下面的 Step 1 + Step 2 几分钟就能把挂掉的服务救回来。
为什么会这样:一个 Postgres 连接是重资源(每个占用约 5-10 MB 服务器内存外加一个后台进程),完全不像一个廉价的 HTTP 请求。你的 Postgres 实例只接受固定数量的直连——Supabase Free 档是 60 个(随 compute 规格变化:Micro 60、Small 90、Medium 120,往上更多,截至 2026 年 6 月)。Serverless 平台在流量突增时会拉起 100 多个互相独立的实例,每个都开自己的直连,瞬间就把上限撑爆。修法不是”买更多连接”,而是在函数和 Postgres 之间放一个 connection pooler(Supabase 内置的 Supavisor),让成千上万的短连接共用一小撮真正的后端连接。
先判断你属于哪一类
| 日志里的症状 | 最可能的原因 | 跳到 |
|---|---|---|
流量高峰时 too many clients already | Serverless 走直连 :5432,没过 pooler | 原因 1 / Step 2 |
| 连接数只涨不降 | 连接泄漏(漏了 release()) | 原因 2 / Step 5 |
prepared statement "s0" already exists / ...does not exist | transaction pooler 下用了 prepared statements | 原因 4 / Step 4 |
pooler 报 MaxClientsInSessionMode / “max clients reached” | pooler 客户端上限被打满,或误用 session mode | 原因 3 |
| 错误集中在整点 | cron / 批处理任务霸占连接池 | 原因 5 |
pg_stat_activity 里出现 pgAdmin/Metabase/DBeaver | GUI 工具挂在 prod 上 | 原因 6 |
新机器根本连不上(ENETUNREACH、IPv6 地址) | 直连需要 IPv6,而你在纯 IPv4 网络 | 见”IPv4 与 IPv6” |
常见原因
按命中率从高到低排列。
1. Serverless 直连 :5432(不走 pooler)
最高频。代码用了 Dashboard 给的 Direct connection 串,每个 Lambda / Worker / Edge 实例各自开一个后端连接。突发流量拉起 100 个实例 → 开 100 个连接 → 撑爆上限。
如何判断: 看连接串里的 host 和端口。db.<ref>.supabase.co:5432 是直连,aws-<region>.pooler.supabase.com:6543 是 transaction pooler。Serverless 必须用后者。
2. 没释放连接 / 长事务
const client = await pool.connect();
const result = await client.query('SELECT ...');
// 漏了 client.release()
每个 request 漏 1 个连接,几小时就耗尽。如果你开了事务(BEGIN)后提前 return/抛错跳过了 COMMIT/ROLLBACK,结果一样——这个后端会永远卡在 idle in transaction。
如何判断: active 连接数单调上涨、从不回落到基线,哪怕流量空闲时也不降。
3. pooler 客户端上限被打满 / 误用 session mode
Supavisor 暴露两个不同的串。6543 端口是 transaction mode——每个事务一结束连接就归还池子,所以少量后端连接能服务上千客户端。pooler.supabase.com host 上的 5432 端口是 session mode——每个客户端在整个会话期间独占一个连接,换回完整的 Postgres 兼容性,但失去了扩展优势。(注意:6543 端口上的 session mode 已于 2025 年 2 月 28 日下线——现在 6543 只支持 transaction,所以你是靠复制哪个端口/串来选模式,而不是靠 pool_mode 查询参数。)
如何判断: pooler 返回 MaxClientsInSessionMode 或 “max clients reached”,或者你看到 serverless 负载指向了 :5432 的 pooler 串。
4. transaction pooler 不支持的 statement
prepared statements、LISTEN/NOTIFY、advisory locks、会话级临时表等在 transaction mode pooler 下都会失效,因为下一个事务可能落到另一个后端连接上。最常见的触发源是 ORM:Prisma,以及 node-postgres 在 pool.query 带 name 时,默认都会创建服务端 prepared statements。
如何判断: 报 prepared statement "s0" already exists 或 prepared statement "s1" does not exist。
5. 后台 cron / 批处理跑慢查询
一个跑 30 秒的定时统计查询,全程占着一个连接。如果几个同时触发、正常流量又要连接,池子就被 cron 吃掉了。
如何判断: 错误紧贴 cron 触发时间(常在整点 :00)成簇出现。
6. 旁路工具(pgAdmin、Metabase、DBeaver)占着连接
你或同事用 GUI 直连 prod 库,每个空闲的 GUI 会话可能占着 5-10 个永不关闭的连接。
如何判断: 跑 SELECT application_name, count(*) FROM pg_stat_activity GROUP BY application_name;,看有没有你认得的工具名。
IPv4 与 IPv6(如果你”根本连不上”先看这段)
很多人在从直连串迁移时会被这个坑住。截至 2026 年 6 月:
- 直连(
db.<ref>.supabase.co:5432)解析到的是 IPv6 地址。如果你的运行环境或网络是纯 IPv4(很多 CI runner、部分公司内网、较旧的 serverless 区域),它会在你撞到连接上限之前就先报ENETUNREACH或 “no route to host”。 - Shared Pooler / Supavisor(
...pooler.supabase.com,5432和6543都算)在所有档位都兼容 IPv4——这也是 serverless 该走它的另一个理由。 - 如果你确实需要在 IPv4 下用直连,可启用付费的 IPv4 add-on,或改用 Dedicated Pooler(与数据库同机部署,延迟比 shared pooler 更低,付费 compute 上可用)。
最短修复路径
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;
-- 终止卡 idle 超过 5 分钟的事务
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND query_start < now() - interval '5 minutes';
这能换来喘息空间让服务恢复,但只是止血带——Step 2-5 才是真正的修复。
Step 2:把 serverless 切到 transaction pooler
在 Supabase Dashboard 顶栏点 Connect,弹窗里列出全部三个串。(同样的串也在 Project Settings → Database → Connection string 下,那里还有一个 Connection pooling 标签页可以调池子设置。)
直连(IPv6,仅限长跑服务器):
postgresql://postgres:[PASSWORD]@db.<ref>.supabase.co:5432/postgres
Session pooler(5432 端口,IPv4,每客户端独占一个连接):
postgres://postgres.<ref>:[PASSWORD]@aws-<region>.pooler.supabase.com:5432/postgres
Transaction pooler(6543 端口,IPv4——serverless 用这个):
postgres://postgres.<ref>:[PASSWORD]@aws-<region>.pooler.supabase.com:6543/postgres
注意用户名不一样:直连用 postgres,两个 pooler 串都用 postgres.<ref>。把你的环境变量(DATABASE_URL)改成 :6543 那个串,然后重新部署。
Step 3:调对池子大小,别一上来就升 plan
现在不再用查询参数选”session 还是 transaction”——而是靠复制 :6543(transaction)还是 :5432 的 pooler 串(session)来选。Serverless 要的是 transaction mode。
如果单个函数实例可能开多于一个连接,给它封顶,免得一群实例各抓一把:
postgres://...@...:6543/postgres?connection_limit=1
对 serverless 来说,每实例 connection_limit=1 是合理默认——每个实例一次只处理一个请求。pooler 的 Pool Size(每租户默认 15,可在 Connection pooling 标签页调)是它向 Postgres 保持的真实后端连接数;Max Client Connections(Free 档默认 200,截至 2026 年 6 月)是同时能挂到 pooler 上的 serverless 客户端数。
Step 4:Prisma / ORM 特殊处理(prepared statements)
transaction mode 用不了服务端 prepared statements,所以 Prisma 需要加 pgbouncer=true(这会让 Prisma 关掉 prepared statements):
// .env
// 运行时查询 → transaction pooler,关掉 prepared statements:
DATABASE_URL="postgres://postgres.<ref>:[PASSWORD]@aws-<region>.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
// migration 需要一个不走池子、支持 session 的连接:
DIRECT_URL="postgres://postgres.<ref>:[PASSWORD]@aws-<region>.pooler.supabase.com:5432/postgres"
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // 应用查询 → :6543 pooler
directUrl = env("DIRECT_URL") // prisma migrate / db push → :5432
}
把 directUrl 指向 session 模式 pooler(:5432),而不是原始直连串——直连 host 需要 IPv6,很多 CI/migration 环境没有。用 node-postgres 时别用命名 prepared statements;用 Drizzle 的 postgres-js driver 时设 prepare: false。
Step 5:query 必须释放
// 错——出错时会泄漏
const client = await pool.connect();
await client.query(/* ... */);
// 没有 finally → 连接永不归还
// 对——保证 release
const client = await pool.connect();
try {
await client.query(/* ... */);
} finally {
client.release();
}
// 最简单——pool.query 自动取出并归还连接
await pool.query('SELECT * FROM users WHERE id = $1', [id]);
只有需要多语句事务时才手动取 client,否则一律优先用 pool.query。
Step 6:监控 + 告警
-- 当前连接按 state 分组
SELECT state, count(*) 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';
连接总数超过上限约 80% 时触发告警。Supabase Dashboard 也在 Reports → Database 里画了连接数曲线。
Step 7:升级 compute(最后手段,不是第一步)
直连上限随 compute 规格变化,而非 plan 名称:大致 60(Micro/Free)、90(Small)、120(Medium),更大实例更高,截至 2026 年 6 月。Pro 档($25/月)让你能抬高 compute 和 pooler 上限。升级能买到余量,但如果 serverless 还在走直连,它只是把下一次故障往后拖——先把 pooling 修对。
如何确认修好了
- 部署完
:6543串后,做一次压测(或者直接在真实流量下刷新),盯着SELECT state, count(*) FROM pg_stat_activity GROUP BY state;——active 数应该稳定在上限以下,而不是一路往上爬。 - 确认部署环境里的连接串 host 以
pooler.supabase.com结尾、端口是6543(去平台的环境变量面板查,别只看本地.env)。 - 让它空闲 10 分钟,再跑一次计数查询——idle-in-transaction 应该回落到接近 0。如果没有,说明你还有
release()泄漏(Step 5)。
预防建议
- Serverless / edge:永远走
:6543transaction pooler,绝不碰直连:5432。 - 只有长跑后端(Fly.io、Railway、带稳定连接池的 VM/EC2 进程)才用直连——而且要么走 IPv6,要么开 IPv4 add-on。
- serverless 里用 ORM 时,每实例设
connection_limit=1,Prisma 加pgbouncer=true。 - 每个 query:
await加try/finally释放,或直接用pool.query。 - 监控连接数,超过上限 80% 告警。
- 给 cron / 批处理独立连接或专用 worker,别和请求流量抢池子。
- 别让笔记本或 GUI 工具挂在 prod 库上——用 Supabase Studio、read replica 或 Dedicated Pooler。
- 大流量项目第一天就估算连接需求;Free 档那 60 个直连消失得很快。
常见问题
5432 和 6543 端口有什么区别?
在 pooler.supabase.com host 上,5432 是 session mode(每客户端独占一个连接,Postgres 功能全开),6543 是 transaction mode(每个事务结束后连接回收,专为 serverless 设计)。而 db.<ref>.supabase.co:5432 是另一个完全绕过 pooler 的直连。
serverless 里 connection_limit 该不该调到 1 以上?
很少需要。每个 serverless 实例一次只处理一个请求,一个连接就够。调高只会让流量突增更快打满 pooler。带内部并发的长跑服务器是例外。
为什么报 prepared statement "s0" already exists?
你的 ORM 或 driver 在创建服务端 prepared statements,而它们在 transaction mode pooler 下活不下来。Prisma 在 DATABASE_URL 加 ?pgbouncer=true;其他 driver 关掉 prepared statements(如 postgres-js 里设 prepare: false)。
切到 pooler 后 migration 跑不了——为什么?
transaction mode 不支持 migration 需要的会话级特性(advisory locks、prepared statements)。让 migration 走一条单独的 session 模式连接——把 directUrl 指向 :5432 的 pooler host(不是 IPv6 直连串)。
连接数明明正常,却还在报 “too many clients”?
你可能撞的是 pooler 的 Max Client Connections,而不是 Postgres 的直连上限;也可能是 GUI 工具或 cron 挂在库上。查 SELECT application_name, count(*) FROM pg_stat_activity GROUP BY application_name;,如果确实是应用流量,就在 Connection pooling 标签页把 pooler 客户端上限调高。
GitHub Actions / CI 里能用直连吗?
通常不行——直连 host 只有 IPv6,很多 CI runner 是纯 IPv4,会报 ENETUNREACH。CI/migration 用 :5432 的 session pooler 串,或开 IPv4 add-on。