Edge Functions worker timeouts and WebSocket drops
Last edited: 8/12/2026
背景 #
🌐 Background
Edge Functions 在由 edge-runtime 管理的 V8 隔离环境中运行。
🌐 Edge Functions run inside V8 isolates managed by a supervisor in edge-runtime.
主管会执行资源限制:
🌐 The supervisor enforces resource limits:
- 挂钟时间(
worker_timeout_ms) - CPU 时间(软限制 + 硬限制)
- 内存使用
如果隔离实例处于空闲状态,它可能会提前退出(EarlyDrop 事件)。当满足以下条件时,隔离实例被认为是空闲的:
🌐 It may retire early (EarlyDrop event) if the isolate is idle Isolate is considered idle if following conditions are met:
- HTTP 响应已经返回了。
- 所有
EdgeRuntime.waitUntil()的承诺都已完成。
如果在资源检查期间两者都为真,即使有打开的 WebSocket 连接,隔离也可以被终止。
🌐 If both are true during a resource check, the isolate can be terminated even with open WebSocket connections.
场景 1:WebSocket 大约在一半的墙上时钟限制时掉线 #
🌐 Scenario 1: WebSocket drops around half of the wall clock limit
症状 #
🌐 Symptoms
- WebSocket 会在一个固定的间隔关闭(通常大约是配置的时钟限制的一半)。
- 日志包含
EarlyDrop或实时时钟警告事件。
根本原因 #
🌐 Root cause
在 Deno.upgradeWebSocket(req) 返回响应后,HTTP 请求就被认为已被确认。如果没有未解决的 waitUntil 工作,工作线程可能看起来空闲,并可能提前被回收。
🌐 After Deno.upgradeWebSocket(req) returns a response, the HTTP request is considered acknowledged. If there is no unresolved waitUntil work, the worker may look idle and be retired early.
要检查什么 #
🌐 What to check
- 把连接使用时长和你的墙上时钟限制对比一下。
- 检查
EarlyDrop在断开连接时间前后的日志。 - 确认没有未解决的与套接字生命周期相关的
EdgeRuntime.waitUntil()承诺。
解决方法 #
🌐 Workaround
保持一个承诺,直到套接字关闭为止。
🌐 Keep a promise pending until the socket closes.
1Deno.serve((req) => {2 const { socket, response } = Deno.upgradeWebSocket(req)34 const socketClosedPromise = new Promise<void>((resolve) => {5 socket.onclose = () => resolve()6 })78 EdgeRuntime.waitUntil(socketClosedPromise)910 socket.onmessage = (event) => {11 socket.send(event.data)12 }1314 return response15})EdgeRuntime.waitUntil()可以防止提前退休,但它不会延长硬性退休年龄限制。
场景 2:功能在固定时间内被终止 #
🌐 Scenario 2: Function killed at a consistent duration
症状 #
🌐 Symptoms
- 函数会在可预测的运行时间失败(例如,总是在差不多同一秒时)。
- 日志包含墙钟关闭的原因。
- 客户可能会收到状态
546或取消错误。
根本原因 #
🌐 Root cause
函数超出了设置的实际时间预算。
🌐 The function exceeded the configured wall clock budget.
变通方法 #
🌐 Workarounds
- 把工作分成更小的部分。
- 把耗时的工作移到异步/后台处理,并尽早返回。
- 尽可能使用上游 API 的流式传输。
- 使用队列(
pg_net、pgmq或 webhooks)进行分块处理。
场景3:函数被CPU限制杀死 #
🌐 Scenario 3: Function killed by CPU limit
症状 #
🌐 Symptoms
- 在高计算任务中出错。
- 日志包括 CPU 软/硬限制事件。
根本原因 #
🌐 Root cause
CPU 时间预算和实际时间预算是独立的。一个函数可能在实际时间耗尽之前就用完了 CPU 时间。
🌐 CPU budget and wall clock budget are independent. A function can run out of CPU time long before wall clock is exhausted.
变通方法 #
🌐 Workarounds
- 把大的同步循环拆成异步块。
- 优化耗费高的路径,避免重复计算。
- 把重负载计算转移到为长时间CPU密集型工作设计的系统上。
场景 4:SSE 或 AI 流在完成前结束 #
🌐 Scenario 4: SSE or AI streams end before completion
症状 #
🌐 Symptoms
- 直播开始了,但很快就结束了。
- 没有最终的
[DONE]标记或正常的关闭标记。
根本原因 #
🌐 Root cause
工人在传送长流时碰到了挂钟或提前退休的情况。
🌐 The worker hits wall clock or early retirement conditions while forwarding a long stream.
解决方法 #
🌐 Workaround
在整个流管道生命周期中保持隔离体存活:
🌐 Keep the isolate alive for the stream piping lifecycle:
1Deno.serve(async (_req) => {2 const upstream = await fetch('https://api.openai.com/v1/chat/completions', {3 method: 'POST',4 headers: {5 'Content-Type': 'application/json',6 Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,7 },8 body: JSON.stringify({ stream: true }),9 })1011 const { readable, writable } = new TransformStream()1213 EdgeRuntime.waitUntil(upstream.body!.pipeTo(writable))1415 return new Response(readable, {16 headers: { 'Content-Type': 'text/event-stream' },17 })18})场景5:冷启动在第一次响应前失败 #
🌐 Scenario 5: Cold starts fail before first response
症状 #
🌐 Symptoms
- 闲置后第一次请求失败(例如,
504或工作线程创建超时)。 - 后续的请求可能会成功。
根本原因 #
🌐 Root cause
大型依赖树或昂贵的顶层初始化可能会超出启动预算。
🌐 Large dependency trees or expensive top-level initialization can exceed startup budget.
变通方法 #
🌐 Workarounds
- 避免慢速的顶层
await工作。 - 在请求处理器里懒加载重量级客户端。
- 减少包和依赖的大小。
- 如果需要的话,保持关键功能处于活跃状态。
主要区别 #
🌐 Key distinctions
EarlyDrop:当工人显得懒散时提前退休。WallClockTime:达到硬性运行时间上限。- CPU 和挂钟时间限制是独立的。
- 升级后,WebSockets 不会被主管追踪请求。
相关资源 #
🌐 Related resources