Supabase Edge Function 调用 LLM 时,最常见的超时场景不是 GPT-4o 返回慢,而是数据库连接池把 150 秒墙钟配额吃掉了。Deno 日志里一条 PostgresError 后紧跟一个未捕获的 timeout,前端只看到 502 Bad Gateway——整个过程里 LLM API 甚至还没收到请求。
这个问题在独立开发者做 AI SaaS 时尤其隐蔽:本地 dev 用 supabase functions serve 不走连接池,一切正常;一部署到生产,Edge Function 走 Supavisor 的 Transaction Mode Pooler,prepared statement 直接被拒绝,客户端反复重试直到超时。
为什么 Transaction Mode 不支持 Prepared Statement?
Supabase 的 Edge Functions 使用连接池而非直接连 PostgreSQL。架构上绕不开:
- Edge Function 实例是短生命周期的(Deno isolate 在处理完请求后可能被回收)
- 如果每个实例都创建独立的长连接,几千个并发请求会把 PostgreSQL 连接数打满
- 所以 Supabase 使用 Supavisor(或 Dedicated Pooler 的 PgBouncer)做连接池代理
Supavisor 连接池在 Transaction Mode 下,一个连接被多个请求复用——请求 A 拿连接→执行 SQL→释放,请求 B 接着用同一个连接。Prepared statement 是绑定在特定连接上的:请求 A 创建的 prepared statement,请求 B 用不了,但客户端不知道,继续用缓存里的 statement name 去调用,就会报错。
错误日志通常是:
PostgresError: prepared statement "s0" does not exist
或者更隐蔽的:
PostgresError: unnamed prepared statement does not exist
后者的触发原因是:postgres.js 客户端默认会隐式创建 unnamed prepared statement 来缓存查询计划。当连接被池回收后,下一个请求带着同一个 statement 名称进来,实际的 PostgreSQL 连接已经换了,语句不存在。
多方案连接配置对比
Edge Function 连数据库有三种路径,各自有不同的适用场景:
| 方案 | 端口 | 连接模式 | Prepared Statement | 适用场景 | 风险 |
|---|---|---|---|---|---|
| Supavisor Transaction Mode | 6543 | 池化事务 | 不支持,必须关闭 | Edge Function 生产默认(推荐) | 需改客户端配置 |
| Supavisor Session Mode | 5432 | 池化会话 | 支持 | 长生命周期应用(非 Edge) | Edge 下连接泄漏 |
| 直连 PostgreSQL | 5432(非池化) | 原始连接 | 支持 | psql、migration、pg_dump | 连接数快速耗尽 |
Edge Function 只有一个正确选择:Transaction Mode 端口 6543 + 关闭 prepared statement。
连接字符串(从 Dashboard → Settings → Database → Connection Pooling 获取):
postgresql://postgres.[project-ref]:[password]@aws-0-us-west-1.pooler.supabase.com:6543/postgres
注意区分:端口 6543 是 Pooler,端口 5432 是直连。很多项目在本地开发时用了 5432,部署时没切 6543,导致 Edge Function 打满连接数。
postgres.js 客户端怎么关 Prepared Statement?
独立开发者最常用的 Postgres 客户端是 postgres.js(Deno 生态首选),一行配置即可关闭:
import postgres from 'https://deno.land/x/[email protected]/mod.js'
const sql = postgres(Deno.env.get('DATABASE_URL')!, {
prepare: false, // Transaction Mode 必须关
max: 5, // 限制并发连接数,默认 10 在 Edge 下太大
idle_timeout: 10, // 空闲连接 10 秒回收
})
如果你用 Supabase 的 js 客户端(@supabase/supabase-js),它的内部 query 不会用 prepared statement,不受这个限制——但它的底层连接也是 Pooled,所以批量操作时仍然要注意连接数量上限。
Edge Function 完整写法:
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import postgres from 'https://deno.land/x/[email protected]/mod.js'
Deno.serve(async (req) => {
const dbUrl = Deno.env.get('DIRECT_URL')! // 用于直连(非池化,本地 dev 用)
const poolUrl = Deno.env.get('DATABASE_URL')! // 用于池化(生产用 6543)
// supabase-js 读写数据
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// postgres.js 跑复杂查询(必须关 prepare)
const sql = postgres(poolUrl, { prepare: false })
const { data: user } = await supabase
.from('users')
.select('*')
.eq('id', req.headers.get('x-user-id'))
.single()
if (!user) {
return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
}
const result = await sql`SELECT * FROM llm_logs WHERE user_id = ${user.id} ORDER BY created_at DESC LIMIT 10`
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' },
})
})
如果按照以上配置后数据库查询恢复正常,但 LLM 调用仍超时,问题就出在 LLM 端。
LLM 调用超过 150 秒怎么分流?
OpenAI 的 GPT-4o、Claude 4.7 Opus 和 DeepSeek-R1 等大模型的推理时间波动很大——高峰期一次 Completion 可能 30-120 秒,加上 prompt 预处理和 response 后处理,很容易逼近 150 秒阈值。
不是在 Edge Function 里加更多超时预算,而是把 LLM 调用从请求链路里拆出来。
方案 A:waitUntil 后台执行(单次 ≤ 400 秒,Pro 套餐)
Deno.serve(async (req) => {
const { prompt } = await req.json()
EdgeRuntime.waitUntil(
(async () => {
const llmRes = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
}),
})
const data = await llmRes.json()
// 结果写到数据库,或通过 Supabase Realtime 推送
const sql = postgres(Deno.env.get('DATABASE_URL')!, { prepare: false })
await sql`INSERT INTO llm_results (prompt, result) VALUES (${prompt}, ${JSON.stringify(data)})`
})()
)
return new Response(JSON.stringify({ status: 'processing' }), { status: 202 })
})
前端通过 Supabase Realtime 订阅 llm_results 表的变化,或者轮询查询状态。
方案 B:OpenAI background: true(无需管理超时)
const res = await fetch('https://api.openai.com/v1/responses', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4o',
input: [{ role: 'user', content: prompt }],
background: true,
}),
})
const { id } = await res.json()
// OpenAI 完成后 POST 到你提供的 webhook URL
// 把 response_id 存起来,前端轮询或 webhook 回调后通知用户
| 策略 | 最长耗时 | 复杂度 | 是否需 Pro | 适用 LLM |
|---|---|---|---|---|
| 直接 Edge 内调用 | ~140 秒 | 低 | Free 够用 | 快模型(gpt-4o-mini) |
| waitUntil 后台 | 400 秒 | 中 | 需要 | 中速模型(gpt-4o) |
| OpenAI background | 无硬限制 | 中 | Free 够用 | OpenAI 生态 |
| 独立长期运行服务 | 不限 | 高 | 任何套餐 | 所有模型 |
独立开发者做 AI SaaS 时,建议初期选用方案 A 或 B,避免过早引入额外服务。
如果团队部署涉及 Supabase 到 OpenAI 的网络稳定性,可以把后台 API 调用放在一条固定路由上,并使用独立开发者出海稳定专线承载 Edge Function 到 LLM 的出口流量。
排查三步:先改连接,再拆 LLM
按概率从高到低:
- 数据库连接占满超时(概率最高):检查连接字符串端口是否 6543,客户端
prepare: false是否设置。Edge Function 日志里看到PostgresError+ connection timeout 属于这一类。 - LLM I/O 阻塞墙钟(概率次高):用
console.time('llm')和console.timeEnd('llm')在 Edge Function 日志里确认 LLM 调用的实际耗时。如果单次超过 120 秒,改用 waitUntil 或 background 模式。 - 冷启动叠加(概率低但影响大):Deno 首次加载依赖可能耗 3-8 秒。如果 LLM 调用 + 冷启动 > 150 秒,用
deno info检查依赖包大小,或者把常用的@supabase/supabase-js和 OpenAI client 拆分导入。