你把一个 LLM 调用、图像处理、或长 API 聚合的端点部署到 Vercel Edge / Cloudflare Workers / Netlify Edge。本地跑 5 秒正常,上生产前几次请求也行。但流量一上去,Vercel 开始报 FUNCTION_INVOCATION_TIMEOUT(HTTP 504),Cloudflare 报 Worker exceeded CPU time limit(Cloudflare Error 1102),Netlify 则是响应被悄悄截断。把同一份代码搬离 edge 就好了。
最快的修复: 如果做的是 LLM 调用、PDF 解析、或任何超过约 10 秒实际计算的活,直接把它从 edge runtime 拿下来。在 Vercel 上删掉 export const runtime = 'edge'(或改成 'nodejs'),重新部署。截至 2026 年 6 月,Node.js runtime 配合 Fluid Compute,所有套餐(含 Hobby)的默认时长都是 300 秒(5 分钟),所以超时通常立刻消失。edge 只留给亚秒级的路由、鉴权、重定向。本文剩下的部分讲的是:当你确实需要留在 edge 上时,怎么绕过那些专属的坑。
理解关键:edge 不是”更快的 Lambda”,它是为低延迟分发设计的运行时,CPU 和时间预算都非常紧。每个平台的上限不同,而坑就在于:它们计的根本不是同一个东西。
各平台限制速查(2026 年 6 月已核实)
| 平台 / runtime | 计什么 | 免费 / 默认 | 上限 |
|---|---|---|---|
| Vercel Edge runtime | 首字节时间,之后 stream | 必须在 25s 内开始返回响应 | 之后可 stream 到 300s |
| Vercel Node.js(Fluid Compute) | wall-clock 总时长 | 300s 默认,所有套餐含 Hobby | Pro/Ent 可设到 800s(1800s 扩展,beta) |
| Cloudflare Workers(Free) | 只计 CPU 时间 | 每请求 10ms CPU | 10ms(硬限制) |
| Cloudflare Workers(Paid) | 只计 CPU 时间 | 30s CPU 默认 | 可调到 5 min(300000ms);HTTP Worker 无硬性 wall-clock 上限 |
| Netlify Edge Functions | 只计 CPU 时间 | 50ms CPU | 须在 40s 内返回 headers,之后可无限 stream |
最常被搞错的两点:(1)Vercel Edge 已经不再是一刀切的 25 秒 wall 限制了。它要求首字节在 25 秒内发出,之后最多可以 stream 到 300 秒。(2)Cloudflare 和 Netlify 计的是 CPU 时间,不是 wall-clock,所以 await fetch(...) 等一个慢上游不会消耗你的 CPU 预算。Cloudflare 报 CPU 超时,说明是你自己的 JS(JSON parse、crypto、热循环)太重,而不是网络等待。
常见原因
按命中率从高到低:
1. 在 edge 跑同步重活(LLM 长响应、图像处理)
一次完整(非流式)LLM 补全经常要 20-60 秒。PDF 解析、图像生成都是几十秒起步。如果你把整个结果攒齐才返回,Vercel Edge 会掐断你,因为首字节根本没在 25 秒内发出去。
如何判断:文件里有 export const runtime = 'edge',handler 里 await 一个非流式的 LLM / 图像调用,然后一次性返回。
2. 上游慢且没设超时
await fetch(upstream) 不带 signal,上游卡死你就跟着卡。某些第三方 API 平时 1 秒,偶尔抽风 60 秒。在 Vercel Edge 上这会冲掉 25 秒首字节窗口;在 Node.js 上则吃掉你的 maxDuration。
如何判断:日志里上游延迟是平时的好几倍;p99 远高于 p50。
3. Cloudflare 是 CPU 限制,不是 wall time
Cloudflare Workers 计的是 CPU 时间(实际计算),不是 wall time。Free plan 每请求只给 10ms CPU,所以一次重的 JSON.parse、大数组 Array.sort、或对大 payload 做 crypto.subtle,看着很快也会超。Paid plan 默认 30s CPU,可调到 5 min。
如何判断:Cloudflare dashboard → Workers & Pages → 你的 Worker → Metrics 里 CPU time 贴着上限;错误是 Error 1102: Worker exceeded CPU time limit。网络等待不计入,所以给 await fetch 加 retry 治不好 1102。
4. 串行调多个上游(应该并发)
const a = await fetch(api1); // 5s
const b = await fetch(api2); // 5s
const c = await fetch(api3); // 5s
// 总 15s,等待全堆一起了
这些应该用 Promise.all 并发跑。
如何判断:互不依赖的 await fetch 一个接一个,后一个根本不需要前一个的结果。
5. 攒齐 response 才返回,没做 streaming
response body 攒齐后才返回。这比逐 chunk stream 慢得多,会把首字节拖过 25 秒 edge 窗口,还可能撞上 Vercel 的 4.5 MB response body 上限(FUNCTION_PAYLOAD_TOO_LARGE,HTTP 413)。
如何判断:客户端等很久才一次性收到全部内容(没有渐进显示);大响应报 413。
6. cold start 偷掉好几秒
首次冷启动可能花 2-5 秒做初始化(bundle 求值、KV / secret 注入),留给业务逻辑的 25 秒首字节窗口就更少了。
如何判断:空闲后第一个请求慢,紧接着的请求快。
最短修复路径
Step 1:先确认真的是 edge 限制
// Vercel: 路由文件顶部有这行 = edge runtime
export const runtime = 'edge';
去掉它(或改成 'nodejs'),重新部署。超时消失,就是 edge 限制。在 Cloudflare 上看错误码:1102 是 CPU 时间超限(你自己的 JS),普通超时则指向 wall-clock 或上游。然后再决定这个端点到底该不该放 edge。
Step 2:没理由就别放 edge
| 它做什么 | 该放哪 |
|---|---|
< 5s、纯路由 / 鉴权 / 重定向 | edge(网络边缘的低延迟才是它的卖点) |
| LLM 调用、PDF 处理、图像处理 | Node.js serverless(Vercel 默认 300s) |
> 5 min | 背景 job(Inngest / cron / queue) |
| 持续连接 / WebSocket | Durable Object / 专用 server |
把 LLM 端点改成 Node.js runtime:
// Vercel Pages Router (pages/api/*)
export const config = { runtime: 'nodejs', maxDuration: 60 };
// Vercel App Router (app/api/*/route.js)
export const runtime = 'nodejs';
// 启用 Fluid Compute(2025 年起默认)后,所有套餐默认 300s。
// Hobby 上限 300s;Pro/Enterprise 可设到 800s。
export const maxDuration = 60;
截至 2026 年 6 月,过去那个”Hobby 10s”的上限已经没了。有了 Fluid Compute,Hobby 默认也是 300s,所以大多数 LLM 端点根本不用设 maxDuration。把它设小只是为了快速失败。
Step 3:必须留 edge,就 stream
把响应做成流式,让首字节在 25 秒窗口内到达。之后 Vercel Edge 允许你继续 stream 到 300s:
export const runtime = 'edge';
export async function POST(req) {
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
stream: true,
messages: [{ role: 'user', content: 'hello' }],
}),
});
// 把上游的 SSE 流直接透传出去,首字节很快就发出。
return new Response(upstream.body, {
headers: { 'content-type': 'text/event-stream' },
});
}
stream 绕开了总时长这个坑,因为平台只卡首字节时间,而且天然适合 edge。同样的写法在 Cloudflare Workers 和 Netlify Edge 上也通用(Netlify 只要你在 40 秒内返回了 headers,流式函数就能一直跑下去)。
Step 4:给每个上游 fetch 加 AbortSignal.timeout
const res = await fetch(upstream, {
signal: AbortSignal.timeout(20_000), // 20 秒后中止
});
别让一个慢上游拖垮整个函数。封装一次,到处复用:
async function fetchWithTimeout(url, options = {}, ms = 20_000) {
return fetch(url, { ...options, signal: AbortSignal.timeout(ms) });
}
AbortSignal.timeout() 在 Vercel Edge runtime、Cloudflare Workers、Netlify Edge(都是 Web 标准运行时)里都有,所以这套写法可移植。被中止的 fetch 会抛 TimeoutError,catch 住它,干净地返回 503 而不是一直挂着。
Step 5:互不依赖的调用就并发
// 慢:10s
const a = await fetch(api1);
const b = await fetch(api2);
// 快:约 5s
const [a, b] = await Promise.all([fetch(api1), fetch(api2)]);
3 个互不依赖的 5 秒上游,并发约 5 秒,串行则 15 秒。如果”一个上游失败不该拖垮其他”,用 Promise.allSettled。
Step 6:超过约 5 分钟的活,改背景任务
如果任务确实可能跑得比 runtime 上限还长,就别在请求里硬扛。入队后立刻返回 jobId,再轮询或推送结果。
// 请求侧:入队,立刻返回 jobId
export async function POST(req) {
const { userId } = await req.json();
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 new Promise(r => setTimeout(r, 2000));
}
}
后台任务可用:
- Inngest:自带 retry 和 step 状态的持久化函数
- Trigger.dev:长任务,模型类似
- Vercel Cron + queue(或 Vercel Workflows,支持可恢复、无时长上限的步骤)
- Cloudflare Queues 或 Workflows(queue consumer 有最多 15 分钟 wall-clock)
怎么确认修好了
- 复现当初触发超时的负载(不是单个热请求)。回放你真实的并发量,或用
npx autocannon -c 20 -d 30 https://your-app/api/endpoint压一压。 - 压测时实时看平台日志:Vercel
vercel logs <deployment-url> --follow,或 Cloudflarenpx wrangler tail。目标是零FUNCTION_INVOCATION_TIMEOUT/1102。 - 看尾部,不要看平均。确认 p99 延迟稳稳压在 runtime 上限之下,因为超时总是先从尾部冒出来。
- 如果改成了 streaming,确认客户端在一两秒内就看到首字节(打开 DevTools → Network → 这个请求应该是数据逐步到达,而不是一次性全到)。
预防建议
- 记住你平台的真实上限(见上表),以及每个上限计的是哪个指标:Vercel Edge = 首字节时间;Cloudflare / Netlify = CPU 时间;Vercel Node.js = wall-clock。
- 任何超过约 10 秒实际工作的,默认放 Node.js,不放 edge。先在 Node.js 验证,有量化理由再迁 edge。
- 每个外部
fetch都加AbortSignal.timeout(5-20s),别让一个慢上游拖垮整个请求。 - review 时抓住一个接一个、互不依赖的
await,迁到Promise.all。 - LLM 和大 payload 端点用流式(SSE),edge 上绝不攒齐整个响应再返回。
- 监控 p95 / p99 延迟而非平均值,在逼近上限前就告警。
- 文档化”哪些端点是 edge、哪些是 Node.js、哪些是 background”,新人照着这个模式写。
常见问题
为什么本地和前几个请求都正常,一上量就挂? 本地没有平台超时,也没有冷启动和资源争用。生产里前几个热请求能压住上限;等并发和尾部延迟一起上来,最慢的那批请求越过上限就 504。一定要按真实并发去测,盯 p99 而不是平均。
我的 Cloudflare Worker 超时,但我根本没做重计算,只是 await fetch,为什么?
Cloudflare 计的是 CPU 时间,不是 wall-clock,网络等待不计入。如果报 Error 1102: Worker exceeded CPU time limit,开销在你自己的 JavaScript 里:大 JSON.parse、对大数组排序 / 循环、或 crypto 运算。HTTP Worker 上的普通超时(不是 1102)通常是上游卡住了。加 AbortSignal.timeout 并减少 Worker 内的计算;Free plan 的 CPU 上限是硬性的 10ms。
Vercel 是不是把函数超时调高了?现在到底是多少?
是的。有了 Fluid Compute(2025 年起默认),Node.js runtime 在所有套餐(含 Hobby)上默认 300s(5 分钟),截至 2026 年 6 月仍如此。Pro 和 Enterprise 可配置到 800s(还有 1800s 的扩展 beta)。Edge runtime 是另一回事:首字节须在 25s 内发出,之后可 stream 到 300s。
我的 LLM 端点到底还该不该放 edge? 通常不该,除非你在做流式且想要最低延迟。最简单可靠的方案是 Node.js runtime(默认 300s)配流式响应。edge 留给亚秒级的路由、鉴权、地理位置、重定向——那里它的全球低延迟才是真正的好处。
怎么跑超过 5 分钟的任务? 别在请求里跑。入队后立刻返回(Step 6),再轮询或推送结果。用 Inngest、Trigger.dev、Cloudflare Queues / Workflows,或 Vercel Workflows——它们支持可恢复、几乎无时长上限的步骤。