Edge function 超时:Vercel / Cloudflare / Netlify 修复

报 FUNCTION_INVOCATION_TIMEOUT 或 504?重活搬离 edge、用 streaming、给每个上游 fetch 加超时。各平台限制已核实(2026 年 6 月)。

你把一个 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 默认,所有套餐含 HobbyPro/Ent 可设到 800s1800s 扩展,beta)
Cloudflare Workers(Free)只计 CPU 时间每请求 10ms CPU10ms(硬限制)
Cloudflare Workers(Paid)只计 CPU 时间30s CPU 默认可调到 5 min300000ms);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)
持续连接 / WebSocketDurable 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));
  }
}

后台任务可用:

怎么确认修好了

  1. 复现当初触发超时的负载(不是单个热请求)。回放你真实的并发量,或用 npx autocannon -c 20 -d 30 https://your-app/api/endpoint 压一压。
  2. 压测时实时看平台日志:Vercel vercel logs <deployment-url> --follow,或 Cloudflare npx wrangler tail。目标是零 FUNCTION_INVOCATION_TIMEOUT / 1102
  3. 看尾部,不要看平均。确认 p99 延迟稳稳压在 runtime 上限之下,因为超时总是先从尾部冒出来。
  4. 如果改成了 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——它们支持可恢复、几乎无时长上限的步骤。

相关阅读

标签: #后端 #排查 #排查