你部署了一个 LLM 调用、图像处理、或长 API 聚合的端点到 Vercel Edge / Cloudflare Workers / Netlify Edge,本地跑 5 秒正常,上生产前几次也行,但流量一上去就开始报 FUNCTION_INVOCATION_TIMEOUT 或 504。把它改回普通 serverless 就好了——这是 edge runtime 的硬限制踩到了。
理解关键:Edge runtime 不是”更快的 Lambda”,它是为低延迟分发设计的,CPU 和 wall-clock 上限非常紧:Vercel Edge 25-30 秒、Cloudflare Workers free plan 10ms CPU / paid 30 秒、Netlify Edge ~50ms CPU。重活搬到 edge 必然超时。
常见原因
按命中率从高到低:
1. 在 edge 跑同步重活(LLM 长响应、图像处理)
LLM API 调用经常 20-60 秒,PDF 解析、图像生成都几十秒起步。edge 的 30 秒上限根本不够。
如何判断:函数文件里有 export const runtime = 'edge',且做的事 > 10 秒。
2. 上游 API 慢 / 没设超时
await fetch(upstream) 没设超时,上游卡死你也跟着卡。某些第三方 API 偶发性慢,平时 1 秒,偶尔 60 秒。
如何判断:日志里上游响应时间 > 平时几倍。
3. CPU 限制比 wall time 更紧
Cloudflare Workers 计的是 CPU 时间(实际计算),不是 wall time。但 free plan 只给 10ms CPU,复杂 JSON parse 或 crypto 就超。
如何判断:Cloudflare dashboard → Workers → 看 CPU time 突破 10ms。
4. 串行调多个上游(应该并发)
const a = await fetch(api1); // 5s
const b = await fetch(api2); // 5s
const c = await fetch(api3); // 5s
// 总 15s
应该 Promise.all 并发跑。
如何判断:代码里 await 一个接一个。
5. 大 payload streaming 不到位
response body 攒齐后才返回,比逐 chunk stream 慢得多,还容易超 buffer 上限。
如何判断:客户端等很久才一次性收到全部内容(没有渐进显示)。
6. cold start 把 30 秒里偷掉好几秒
edge 第一次冷启动可能用掉 2-5 秒初始化,留给业务的时间就只有 25 秒。
如何判断:第一次请求慢,后续快。
最短修复路径
Step 1:先确认是不是 edge 限制问题
// Vercel: 函数文件顶部有这行 = edge
export const runtime = 'edge';
去掉这行或改成 'nodejs',重新部署。如果好了 = 确认是 edge 限制。然后看后续步骤决定要不要保留 edge。
Step 2:能不放 edge 就别放
判断标准:
做什么 去哪
==================================================
< 5 秒、纯路由 / 鉴权 edge(低延迟分发是亮点)
LLM 调用、PDF 处理 nodejs serverless(30s-15min)
> 1 min 任务 背景 job(Inngest / Cron / Queue)
持续连接 / WebSocket durable object / 专用 server
把 LLM endpoint 改成普通 serverless:
// Vercel Pages Router
export const config = { runtime: 'nodejs', maxDuration: 60 };
// Vercel App Router
export const runtime = 'nodejs';
export const maxDuration = 60; // Hobby 限 10s, Pro 60s, Enterprise 900s
Step 3:必须留 edge 就 stream
LLM 响应用 SSE / streaming,让首字节快速到达:
export const runtime = 'edge';
export async function POST(req) {
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ ..., stream: true }),
});
return new Response(upstream.body, {
headers: { 'Content-Type': 'text/event-stream' },
});
}
stream 不算总时长(首字节到了就 OK),edge 30s 限制对 streaming 更友好。
Step 4:所有上游 fetch 加 AbortSignal.timeout
const res = await fetch(upstream, {
signal: AbortSignal.timeout(20_000), // 20 秒
});
避免上游慢拖垮你的函数。结合 retry:
async function fetchWithTimeout(url, options, ms = 20_000) {
return fetch(url, {
...options,
signal: AbortSignal.timeout(ms),
});
}
Step 5:能并发就并发
// 慢
const a = await fetch(api1);
const b = await fetch(api2);
// 快
const [a, b] = await Promise.all([fetch(api1), fetch(api2)]);
3 个 5 秒上游 → 5 秒(并发)vs 15 秒(串行)。
Step 6:> 30s 改背景任务
// 请求侧:立刻返 jobId
export async function POST(req) {
const jobId = await enqueue({ task: 'generate-report', userId: ... });
return Response.json({ jobId });
}
// 客户端轮询
async function poll(jobId) {
while (true) {
const { status, result } = await fetch(`/api/jobs/${jobId}`).then(r => r.json());
if (status === 'done') return result;
if (status === 'failed') throw new Error('job failed');
await sleep(2000);
}
}
后台任务用:
- Inngest:函数式背景任务,自带 retry / SDK
- Trigger.dev:类似 Inngest
- Vercel Cron + Queue
- Cloudflare Queues
预防建议
- 熟记你平台的 edge 上限(Vercel 25-30s、Cloudflare CPU 10ms-50ms-30s 看 plan、Netlify ~50ms)
- 任何 > 10 秒的工作默认不放 edge,先放 nodejs 验证再考虑迁移
- 所有外部 fetch 都加超时(5-20s),别让一个慢上游拖垮全局
- 串行的上游 await 用 eslint 规则警告,推 Promise.all
- LLM / stream 类用 SSE,不要等 full response
- 监控 p95 / p99 延迟而非 avg,edge 超时通常发生在 p99
- 文档化”哪些 endpoint 是 edge、哪些是 nodejs、哪些是 background”,新人不要乱套