Edge function 超时:3 个原因 + 修复路径

Edge function 超过平台限制——重活别放 edge / 加超时 / 用 streaming。

你部署了一个 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”,新人不要乱套

相关阅读

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