某个 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.toml 写 timeout 是不够的)。如果热调用本来就要更久,那它根本不该是同步函数,应该放到后台函数(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,并改用机场代码式的区域名(cmh、iad、dub、fra、sfo、nrt、syd)—— 详见步骤 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.toml 写 timeout 并不生效。需要更久就用后台函数(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 failed 和 Vercel 500 errors 看 Vercel 端的对应处理;懒加载和减小 bundle 这两招完全通用。