Netlify Function 冷启动 10 秒超时 —— 排查与修复

Netlify Function 本地正常,但闲置后首次请求返回 502 报 'Task timed out after 10.00 seconds'。最快的修复:把重型 SDK 改成懒加载、移除 aws-sdk v2,让冷启动初始化塞进 10 秒同步上限。

某个 Netlify Function 本地跑 200 毫秒、热调用 300 毫秒,但闲置一段时间后首次请求返回 502,日志里写着 Task timed out after 10.00 seconds。一分钟内再请求,同样的代码秒回。函数本身并不慢,慢的是冷启动开销,而这个开销超过了 Netlify 同步函数默认的 10 秒上限。原因几乎都是模块顶层引入太重(庞大的 SDK、Prisma 客户端、OpenAI/Anthropic 客户端做 DNS 预热),顶层 await 卡在慢上游,或者依赖里被偷偷塞进了 aws-sdk v2。

最快的修复(约 80% 的情况都管用):把任何重型 SDK 的顶层 import 改成 handler 内部的懒加载 import()(下面步骤 2),再跑 npm ls aws-sdk 确认 bundle 里没有 v2 巨无霸(步骤 3)。光这两步就能把冷启动初始化从 4-6 秒降到 1 秒以内。10 秒上限本身是真的,但并非绝对:在 Pro 套餐(截至 2026 年 6 月每月 $20,自 2026 年 4 月 Netlify 取消按席位计费后改为整个组织一口价) 上,Netlify 可以把同步上限提到 26 秒,而后台函数最长能跑 15 分钟 —— 但提高上限只是治标,所以先把初始化重量降下来。

先判断你属于哪一类

动手改代码前,先对号入座。绝大多数”超时”其实是初始化太重,而不是 handler 逻辑慢。

症状最可能的原因跳到
闲置后首次请求失败,重试秒回顶层引入太重步骤 1、步骤 2
bundle(du -sh)超过 10 MB夹带了 aws-sdk v2步骤 3
连热调用都要 8-15 秒runtime 选错(该用后台函数)步骤 4
init 里有顶层 await fetch(...)模块作用域卡在阻塞式上游步骤 2
冷调用慢,DB/KV 在另一个区域区域不匹配步骤 5
init 本身很快(< 1 s)但业务确实重提高上限或改后台函数步骤 4

常见原因

按同步 Netlify Function 的实际频率排序。截至 2026 年 6 月,函数默认 runtime 是 Node 22(除非你通过 AWS_LAMBDA_JS_RUNTIME 环境变量另外指定,例如 nodejs20.x,否则 Netlify 回落到 nodejs22.x)。

1. 顶层引入太重,冷启动全部执行

文件顶部每一个 import 都会在 init 阶段执行,handler 还没开始就先把这些跑完。一个 import { PrismaClient } from '@prisma/client'import OpenAI from 'openai' 就能把冷启动拖长 2-6 秒,因为这些 SDK 会立刻做 DNS 解析、加载大型 JSON、预热 TLS。

如何识别:在文件的第一行加 console.time('init'),导出 handler 之前加 console.timeEnd('init')。如果打印的时间 >= 4 s,问题就在 init。

2. 顶层 await 卡在慢上游或不可达的远端

const config = await fetch(CONFIG_URL).then(r => r.json()) 这种放在模块作用域的 await 会阻塞 init,直到上游返回。如果上游跨区域或挂了,冷启动会把整 10 秒都耗在这里。

如何识别:搜索函数体之外的 await,注释掉重新部署。如果冷启动降到 2 秒以下,就是它。

3. 同步上限 vs 后台函数

标准 Netlify Function 在 Free、Starter、Personal 套餐上,总执行(含 init)上限是 10 秒(这就是 Task timed out after 10.00 seconds 这串字面值背后的上限)。在 Pro 套餐上,同步上限可以提到 26 秒(截至 2026 年的多个 Netlify 支持帖确认,目前要通过 Netlify 控制台 / 工单申请开通 —— 多数账号下仅在 netlify.tomltimeout 是不够的)。如果热调用本来就要更久,那它根本不该是同步函数,应该放到后台函数(15 分钟上限,立即返回 202)或边缘函数(init 50 毫秒以内,跑在 Deno 上)。

如何识别:函数做的就是真重活,热的时候也要 8-15 秒。这不是冷启动问题,是 runtime 选错了。

4. 依赖里夹带了 aws-sdk v2

某些老库(mailgun-js、部分分析 SDK)会牵连 aws-sdk v2 —— 一个约 50 MB 的巨无霸,冷启动时仅解析就要 3-5 秒。Netlify 打包器(zip-it-and-ship-it,底层用 esbuild)并不一定能把它 tree-shake 掉。

如何识别ls -lah .netlify/functions-internal/<fn>/ 看打包后的体积,或者 du -sh node_modules/aws-sdk。打包后超过 10 MB 基本就是它。

5. SDK 内部 DNS 解析卡住

OpenAI、Anthropic、Stripe、Twilio 这些 SDK 第一次发请求时会建立 HTTPS 连接。如果函数所在区域出站 DNS 解析 api.openai.com 之类的域名比较慢(少见,但 Netlify 某些区域故障时会发生 —— 可查 Netlify 状态页),冷启动后的第一次请求会卡 4-8 秒。

如何识别:在第一次调用 SDK 的地方包一层 console.time('first-api-call'),比较冷调用和热调用的差异。

6. 同步读取打进 bundle 的大文件

模块作用域里写 fs.readFileSync('./prompts.json'),文件 5 MB 以上,Lambda 冷缓存下要几百毫秒,叠加其他 init 开销就会把预算打穿。

如何识别:在 handler 之外 grep readFileSync。把大文件读移进 handler 并加模块级缓存。

7. 函数区域和数据上游不在同一区域

函数默认跑在 US East(cmh,俄亥俄),如果你的 Postgres / Redis / KV 在欧洲或亚洲,每次冷调用每条 init 查询都要付 100-150 毫秒的 RTT,几条下来就上 1 秒。

如何识别:对比 netlify.toml[functions.<name>]region 和数据库所在区域。注意 Netlify 已把这个键从 preferred_region 改名为 region,并改用机场代码式的区域名(cmhiaddubfrasfonrtsyd)—— 详见步骤 5。

开始排查前

  • 先确认是冷启动问题:请求一次,等 15-20 分钟,再请求。若只有闲置后那次首发超时,那就是冷启动。
  • 确认确切的 Node runtime(构建从 .nvmrc / NODE_VERSION 读取;截至 2026 年 6 月,函数 runtime 回落到 nodejs22.x)。主版本之间冷启动只差几百毫秒,相比初始化重量几乎可以忽略。
  • 抓一条失败的函数日志,包含 Task timed out after 10.00 seconds 行和 request ID。
  • 弄清楚函数类型:同步、定时(30 秒上限)、还是后台(15 分钟上限)。各自上限不同。

需要收集的信息

  • netlify.toml[functions][build] 段。
  • 函数文件顶部的 import 列表。
  • 打包体积:ls -lah .netlify/functions-internal/<fn>/
  • package.json 依赖以及任何 peer/transitive 的 aws-sdk
  • 失败 request ID 附近的函数日志(init 时间 + handler 时间)。
  • 函数区域 vs 上游(DB、KV、API)区域。
  • 你的 Netlify 套餐(Free/Personal 同步上限 10 秒;Pro 可提到 26 秒)。

分步修复

按性价比从高到低排序。

步骤 1:测量 init 阶段耗时

在函数文件最顶部插入:

const __init = Date.now();
import OpenAI from "openai";
// ...其他 import
console.log(`[init] imports done in ${Date.now() - __init}ms`);

export const handler = async (event) => {
  const __handler = Date.now();
  // ...
  console.log(`[handler] done in ${Date.now() - __handler}ms`);
};

部署后等 20 分钟再请求一次。[init] 那行如果 >= 3 s,问题就是 import 太重;如果 init 没问题但 handler 慢,那是另一类问题。

步骤 2:把重型 SDK 改成懒加载

把顶层 import 改成首次使用时再 import()

let _openai: import("openai").OpenAI | null = null;
async function getOpenAI() {
  if (!_openai) {
    const { default: OpenAI } = await import("openai");
    _openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  }
  return _openai;
}

export const handler = async (event) => {
  const openai = await getOpenAI();
  // ...
};

handler 第一次会付 import 代价,但 init 从 4-5 秒降到 500 毫秒以内。热调用代价为零。

步骤 3:把 aws-sdk v2 从 bundle 里剔除

先找出谁拖进来的:

npm ls aws-sdk

替换成模块化的 v3 客户端,或换更轻量的库:

npm uninstall mailgun-js
npm install mailgun.js form-data

确认 bundle 缩小:

netlify build
du -sh .netlify/functions-internal/<fn>/

通常从 30-60 MB 降到 5 MB 以内,冷启动随之下降 2-3 秒。

步骤 4:把长任务搬到后台函数

如果 handler 确实需要超过同步上限,就改成后台函数(15 分钟上限)。截至 2026 年 6 月,标记后台函数有两种方式:

新写法 —— 在函数导出的 config 里设 background: true(Netlify 现在推荐这种,优于文件名技巧):

import type { Config } from "@netlify/functions";

export default async (req: Request) => {
  // 这里放长任务
};

export const config: Config = { background: true, path: "/process-upload" };

老写法仍然有效 —— 文件名加 -background 后缀:

netlify/functions/process-upload.ts          → 10 秒上限
netlify/functions/process-upload-background.ts → 15 分钟上限

两种方式调用都会立刻返回 202,函数在后台继续跑。配一个状态接口 + 一处 Netlify Blobs / KV 写入,让客户端轮询结果。同类思路可参考 edge function timeout 在 Vercel 端的处理。

步骤 5:把函数区域对齐到数据上游

Netlify 已把按函数设置区域的键从 preferred_region 改名为 region,并改用机场代码(cmh = US East/俄亥俄,默认;iad = US East/弗吉尼亚北部;dub = EU/爱尔兰;fra = EU/法兰克福;sfo = US West;nrt = 东京;syd = 悉尼)。在 netlify.toml 里:

[functions]
  node_bundler = "esbuild"

[functions."process-upload"]
  # 与主 DB / KV 区域对齐
  region = "iad"

重新部署。冷启动 handler 延迟会按 RTT 乘以 init 查询条数下降。(按函数选区域需要付费套餐;免费档函数固定在默认区域。)

步骤 6:保持 SDK 客户端在模块作用域但避免构造时做活儿

如果 init 体积可以接受,但希望热调用极快,可以把客户端留在模块作用域,但不要在构造时调任何远端:

import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// 不要在顶层写 `await openai.models.list()` —— 每次冷启动都会跑。

export const handler = async () => {
  // 第一次真正发请求放这里,不要放 init。
  const r = await openai.chat.completions.create({ /* ... */ });
};

步骤 7:定时 ping 保温(最后手段)

如果是面向用户的接口、冷启动延迟无法接受,用 Netlify 定时函数每 5 分钟打一次:

// netlify/functions/warm-up.ts
import type { Config } from "@netlify/functions";
export default async () => {
  await fetch(`${process.env.URL}/.netlify/functions/<critical-fn>?warmup=1`);
};
export const config: Config = { schedule: "*/5 * * * *" };

目标函数里判断 event.queryStringParameters?.warmup === "1",立刻 200 返回,不做真实业务。

步骤 8:在 Pro 套餐上提高同步上限(仅在初始化已修好之后)

如果 init 已经在 1 秒以内,而 handler 本身确实需要超过 10 秒(慢的第三方 API、很重的 DB 聚合),可升级到 Pro 套餐(截至 2026 年 6 月每月 $20,自 2026 年 4 月 Netlify 取消按席位计费后为整个组织一口价),并通过 Netlify 控制台或工单申请 26 秒 同步上限。别一上来就用这招:26 秒的上限在 12 秒冷启动初始化面前照样超时,而且花钱去掩盖一个 bundle 问题不划算。只在那种确实慢、又没法搬到后台函数的业务上用它。

如何确认已修好

  • 冷启动闭环:部署后等 20 分钟再请求,整体响应在 4 秒以内。
  • 日志里看到 [init] imports done in 小于 1500 毫秒,没有 Task timed out
  • 60 秒内连续请求(热调用)500 毫秒以内。
  • du -sh .netlify/functions-internal/<fn>/ 显示 bundle 在 10 MB 以下。
  • 把”闲置再请求”测试重复三次;如果三次里偶发一次超时,说明 init 仍贴着上限。

长期预防

  • serverless 函数严禁在模块顶层写 await
  • 任何 200 kB 以上的 SDK 默认走 import() 懒加载,init 阶段保持轻。
  • 每次依赖变更后跑 npm ls aws-sdk,出现 v2 当作发布阻塞。
  • 立一条硬规则:热调用就要 5 秒以上的函数走后台或边缘 runtime。
  • CI 加一道冷启动探针:部署 preview,等 15 分钟,请求接口,超过 5 秒就 fail。
  • 函数日志接到 log drain,便于历史性 grep Task timed out,不只是看实时。

常见坑

  • 以为是”代码慢”,结果一通重写 handler,但 90% 时间都耗在 init。
  • 提高函数内存以为能救冷启动;对 50 MB 的 bundle 来说只是缓解。
  • 用后台函数处理需要同步返回的请求 —— 客户端只收到 202,拿不到结果。
  • 忘了 netlify dev 是常驻进程,冷启动 bug 只在生产环境才会出现。
  • 把保温 ping 当作主要修复方案而忽视 40 MB bundle —— 账单翻倍,而 ping 一停问题立刻复现。

常见问答

Q: 能不能把同步 Netlify Function 的 10 秒上限调大?

有时候可以。Free、Personal、Starter 套餐默认 10 秒(这就是 Task timed out after 10.00 seconds 背后的上限)。在 Pro 套餐上,Netlify 可以把同步上限提到 26 秒 —— 截至 2026 年 6 月,要通过控制台或工单申请开通,因为多数账号下仅在 netlify.tomltimeout 并不生效。需要更久就用后台函数(15 分钟),或者把慢任务从请求链路移走。如果光初始化就超过上限,调大上限也救不了。

Q: 我函数 bundle 才 2 MB,为什么冷启动还要 6 秒?

体积只是一个因素。顶层 await、SDK 构造里的活儿、TLS 握手到上游往往才是大头。用 Date.now() 打点对比 import vs handler 时间,多数团队会发现 init 占了 80% 以上。

Q: 换更新的 Node 版本冷启动会更快吗?

略快,每升一个主版本在新容器上大约快 100-300 毫秒。截至 2026 年 6 月,函数 runtime 默认 nodejs22.x,你可以用 AWS_LAMBDA_JS_RUNTIME 环境变量指定别的版本(要在 Netlify UI/CLI 里设,不能写在 netlify.toml)。但这些都救不了 12 秒的 init,所以先把 init 重量降下来,runtime 版本只是误差。

Q: 为什么 netlify dev 没事,生产环境却超时?

netlify dev 在本地一个常驻进程里把函数跑成热的,所以冷启动初始化开销在本地根本不会发生。冷启动超时只在生产环境闲置后的新容器上才出现。一定要用”部署 → 闲置 → 请求”这个闭环来复现,而不是用 netlify dev

Q: Vercel Functions 会不会有同样问题?

会,只是上限不一样。参考 Vercel build failedVercel 500 errors 看 Vercel 端的对应处理;懒加载和减小 bundle 这两招完全通用。

标签: #排查 #netlify #serverless #cold-start #timeout