Durable Objects 怎么让 WebSocket 休眠?

朴素写法 server.accept() 接受 WebSocket 之后,Durable Object(下称 DO)就一直驻留内存并持续计费,哪怕所有人都沉默。

所以答案是:改用 Hibernation API。用 ctx.acceptWebSocket() 接受连接,把处理逻辑放进类的 webSocketMessage、webSocketClose、webSocketError 方法。 对象空闲后可以从内存中移除,客户端连接依然保持,来消息时运行时重建对象。

代价是:重建意味着构造函数重新执行,内存里的状态没了。下面从写法讲起,再说哪些东西会丢、哪些会挡住休眠。房间这类场景的整体思路见 什么样的需求适合交给 Durable Objects?。

Hibernation 的写法是什么样的?

export class Room extends DurableObject<Env> {
  async fetch(_req: Request) {
    const [client, server] = Object.values(new WebSocketPair());
    this.ctx.acceptWebSocket(server, ["room"]);
    this.ctx.setWebSocketAutoResponse(
      new WebSocketRequestResponsePair("ping", "pong"),
    );
    server.serializeAttachment({ joinedAt: Date.now() });
    return new Response(null, { status: 101, webSocket: client });
  }

  webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
    const who = ws.deserializeAttachment();
    for (const peer of this.ctx.getWebSockets("room")) {
      peer.send(`${JSON.stringify(who)}:${msg}`);
    }
  }

  webSocketClose(ws: WebSocket, code: number, reason: string) {
    ws.close(code, reason);
  }
}

我用两个 WebSocket 客户端在本地验证过:一个发 hello,两个客户端都收到带附件的广播;发 ping 立即得到自动应答 pong。休眠本身,也就是空闲后被移出内存,在本地不容易观察,这部分我只能引用文档。

休眠之后,哪些状态会丢?

内存状态全丢。构造函数会重跑,成员列表、游标之类如果只放在内存里,就没了。常见的误解是以为休眠后「房间状态」还在。

要么每次用 ctx.getWebSockets() 重建,要么落存储。每个连接的小元数据放进 serializeAttachment,上限 16384 字节;更大的数据放存储,附件里只存键。另外,连接任何一端关闭,附件就消失。

什么会阻止对象休眠?

官方文档列出的情形有:未完成的 setTimeout、setInterval,进行中的 I/O,未完成的 waitUntil,出站连接,以及使用标准 WebSocket API。生命周期页给出的休眠触发约为空闲 10 秒,并注明具体值由运行时决定、可能变化。

休眠怎么计费?

休眠期间不累计时长计费。WebSocket 连接建立算一次请求,入站消息按 20:1 折算为请求,出站消息与协议 ping 不计,setWebSocketAutoResponse 的自动应答不产生时长费用。价格以官方定价页为准。

部署新版本和关闭连接有什么要注意?

部署新版本会重启所有对象并断开所有 WebSocket,客户端要有重连逻辑。

关闭帧的行为跟兼容日期有关:兼容日期 2026-04-07 及以后,运行时会自动回复 Close 帧;更早的日期要在 webSocketClose 里手动 close,否则客户端可能看到 1006。

两个客户端的广播和 ping/pong 自动应答,我在本地 wrangler dev 上跑过。休眠本身、阻止休眠的条件、10 秒的触发值、16384 字节的附件上限、计费规则、部署断连和兼容日期行为,都来自官方文档,我没有验证过。限额与计费会变化,以 2026-10 的官方文档为准。