mcpField
C
cloudflare-durable-objects

cloudflare-durable-objects

github.com/jezweb云与 DevOps1.1k332 次安装3 周前更新
代码仓库

关于 cloudflare-durable-objects

面向 Cloudflare Workers 开发者的 Durable Objects 实践指南,涵盖持久化状态、并发、WebSocket 休眠、定时任务和迁移。提供 API 用法、架构模式及常见错误预防建议。

主要功能

  1. 01SQLite 与键值存储
  2. 02WebSocket 休眠与状态恢复
  3. 03Alarm 定时任务
  4. 04RPC 与 HTTP Fetch 模式
  5. 05对象 ID 与迁移管理

适用场景

  1. 01为聊天室保存连接元数据
  2. 02构建会话管理和限流逻辑
  3. 03使用 Alarm 批处理或清理数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-durable-objects

安装后在 Claude Code 中输入/cloudflare-durable-objects确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
references/
alarms-api.md
best-practices.md
migrations-guide.md
rpc-patterns.md
state-api-reference.md
top-errors.md
websocket-hibernation.md
wrangler-commands.md
rules/
cloudflare-durable-objects.md
scripts/
check-versions.sh
templates/
alarms-api-do.ts
basic-do.ts
location-hints.ts
multi-do-coordination.ts
package.json
rpc-vs-fetch.ts
state-api-patterns.ts
websocket-hibernation-do.ts

详细介绍

Cloudflare Durable Objects

状态:已可用于生产 ✅ 最后更新:2026-01-21 依赖项:cloudflare-worker-base(推荐) 最新版本:[email protected]、@cloudflare/[email protected] 官方文档:https://developers.cloudflare.com/durable-objects/

近期更新(2025):

  • 2025 年 10 月:WebSocket 消息大小从 1 MiB 提升至 32 MiB;为 SQLite DO 提供 Data Studio UI(可在仪表板中查看/编辑存储)
  • 2025 年 8 月:为命名 DO 提供 getByName() API 快捷方式
  • 2025 年 6 月:推出 @cloudflare/actors 库(测试版)——推荐的 SDK,支持迁移、闹钟和 Actor 类模式。注意:稳定性仍处于测试阶段——投入生产前请查看活跃问题(RPC 序列化、vitest 集成、内存管理)
  • 2025 年 5 月:Durable Objects 支持 Python Workers
  • 2025 年 4 月:SQLite 正式发布,存储容量为 10GB(从测试版升级为正式版,容量从 1GB 提升至 10GB),免费套餐可用
  • 2025 年 2 月:支持 PRAGMA optimize;通过参考 ID 改进错误诊断

快速入门

搭建新的 DO 项目:

npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts

或添加到现有 Worker:

// src/counter.ts - Durable Object class
import { DurableObject } from 'cloudflare:workers';

export class Counter extends DurableObject {
  async increment(): Promise<number> {
    let value = (await this.ctx.storage.get<number>('value')) || 0;
    await this.ctx.storage.put('value', ++value);
    return value;
  }
}
export default Counter;  // CRITICAL: Export required
// wrangler.jsonc - Configuration
{
  "durable_objects": {
    "bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Counter"] }  // SQLite backend (10GB limit)
  ]
}
// src/index.ts - Worker
import { Counter } from './counter';
export { Counter };

export default {
  async fetch(request: Request, env: { COUNTER: DurableObjectNamespace<Counter> }) {
    const stub = env.COUNTER.getByName('global-counter');  // Aug 2025: getByName() shortcut
    return new Response(`Count: ${await stub.increment()}`);
  }
};

DO 类基础

import { DurableObject } from 'cloudflare:workers';

export class MyDO extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);  // REQUIRED first line

    // Load state before requests (optional)
    ctx.blockConcurrencyWhile(async () => {
      this.value = await ctx.storage.get('key') || defaultValue;
    });
  }

  // RPC methods (recommended)
  async myMethod(): Promise<string> { return 'Hello'; }

  // HTTP fetch handler (optional)
  async fetch(request: Request): Promise<Response> { return new Response('OK'); }
}

export default MyDO;  // CRITICAL: Export required

// Worker must export DO class too
import { MyDO } from './my-do';
export { MyDO };

构造函数规则:

  • ✅ 首先调用 super(ctx, env)
  • ✅ 保持精简——繁重工作会阻塞休眠唤醒
  • ✅ 使用 ctx.blockConcurrencyWhile() 初始化存储
  • ❌ 不要使用 setTimeout/setInterval(请改用闹钟)
  • ❌ 使用 WebSocket 时不要依赖内存状态(应持久化到存储中)

存储 API

提供两种后端:

  • SQLite(推荐):10GB 存储、SQL 查询、原子操作、PITR
  • KV:128MB 存储,仅支持键值存储

在迁移中启用 SQLite:

{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }] }
SQL API(SQLite 后端)
export class MyDO extends DurableObject {
  sql: SqlStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;

    this.sql.exec(`
      CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, text TEXT, created_at INTEGER);
      CREATE INDEX IF NOT EXISTS idx_created ON messages(created_at);
      PRAGMA optimize;  // Feb 2025: Query performance optimization
    `);
  }

  async addMessage(text: string): Promise<number> {
    const cursor = this.sql.exec('INSERT INTO messages (text, created_at) VALUES (?, ?) RETURNING id', text, Date.now());
    return cursor.one<{ id: number }>().id;
  }

  async getMessages(limit = 50): Promise<any[]> {
    return this.sql.exec('SELECT * FROM messages ORDER BY created_at DESC LIMIT ?', limit).toArray();
  }
}

SQL 方法:

  • sql.exec(query, ...params) → 游标
  • cursor.one<T>() → 单行(若无结果则抛出异常)
  • cursor.one<T>({ allowNone: true }) → 单行或 null
  • cursor.toArray<T>() → 所有行
  • ctx.storage.transactionSync(() => { ... }) → 多语句原子操作

最佳实践:

  • ✅ 使用 ? 占位符执行参数化查询
  • ✅ 为频繁查询的列创建索引
  • ✅ 更改架构后使用 PRAGMA optimize
  • ✅ 在表定义中添加 STRICT 关键字,以强制执行类型亲和性并及早发现类型不匹配
  • ✅ 将布尔值转换为整数(0/1)——在 SQLite 后端中,布尔值会以字符串 "true"/"false" 的形式绑定
键值 API(两种后端均支持)
// Single operations
await this.ctx.storage.put('key', value);
const value = await this.ctx.storage.get<T>('key');
await this.ctx.storage.delete('key');

// Batch operations
await this.ctx.storage.put({ key1: val1, key2: val2 });
const map = await this.ctx.storage.get(['key1', 'key2']);
await this.ctx.storage.delete(['key1', 'key2']);

// List and delete all
const map = await this.ctx.storage.list({ prefix: 'user:', limit: 100 });
await this.ctx.storage.deleteAll();  // Atomic on SQLite only

// Transactions
await this.ctx.storage.transaction(async (txn) => {
  await txn.put('key1', val1);
  await txn.put('key2', val2);
});

**存储限制:**SQLite 为 10GB(2025 年 4 月正式发布)| KV 为 128MB


WebSocket 休眠 API

功能:

  • 每个实例支持数千个 WebSocket 连接
  • 空闲时(约 10 秒无活动)进入休眠以节省成本
  • 收到消息时自动唤醒
  • 消息大小限制:32 MiB(2025 年 10 月从 1 MiB 提升)

工作原理:

  1. 活跃 → 处理消息
  2. 空闲 → 约 10 秒无活动
  3. 休眠 → 清除内存状态,WebSocket 保持连接
  4. 唤醒 → 收到消息 → 执行构造函数 → 调用处理程序

重要:休眠时会丢失内存状态。使用 serializeAttachment() 持久化每个 WebSocket 的元数据。

休眠安全模式
export class ChatRoom extends DurableObject {
  sessions: Map<WebSocket, { userId: string; username: string }>;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sessions = new Map();

    // CRITICAL: Restore WebSocket metadata after hibernation
    ctx.getWebSockets().forEach((ws) => {
      this.sessions.set(ws, ws.deserializeAttachment());
    });
  }

  async fetch(request: Request): Promise<Response> {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    const url = new URL(request.url);
    const metadata = { userId: url.searchParams.get('userId'), username: url.searchParams.get('username') };

    // CRITICAL: Use ctx.acceptWebSocket(), NOT ws.accept()
    this.ctx.acceptWebSocket(server);
    server.serializeAttachment(metadata);  // Persist across hibernation
    this.sessions.set(server, metadata);

    return new Response(null, { status: 101, webSocket: client });
  }

  async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
    const session = this.sessions.get(ws);
    // Handle message (max 32 MiB since Oct 2025)
  }

  async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
    this.sessions.delete(ws);
    ws.close(code, 'Closing');
  }

  async webSocketError(ws: WebSocket, error: any): Promise<void> {
    this.sessions.delete(ws);
  }
}

休眠规则:

  • ✅ ctx.acceptWebSocket(ws) ——启用休眠
  • ✅ ws.serializeAttachment(data) ——持久化元数据
  • ✅ ctx.getWebSockets().forEach() ——在构造函数中恢复
  • ✅ 使用闹钟代替 setTimeout/setInterval
  • ❌ ws.accept() ——标准 API,不支持休眠
  • ❌ setTimeout/setInterval ——会阻止休眠
  • ❌ 正在进行的 fetch() ——会阻止休眠

Alarms API

安排 DO 在未来某个时间唤醒。**适用于:**批处理、清理、提醒、定期任务。

export class Batcher extends DurableObject {
  async addItem(item: string): Promise<void> {
    // Add to buffer
    const buffer = await this.ctx.storage.get<string[]>('buffer') || [];
    buffer.push(item);
    await this.ctx.storage.put('buffer', buffer);

    // Schedule alarm if not set
    if ((await this.ctx.storage.getAlarm()) === null) {
      await this.ctx.storage.setAlarm(Date.now() + 10000);  // 10 seconds
    }
  }

  async alarm(info: { retryCount: number; isRetry: boolean }): Promise<void> {
    if (info.retryCount > 3) return;  // Give up after 3 retries

    const buffer = await this.ctx.storage.get<string[]>('buffer') || [];
    await this.processBatch(buffer);
    await this.ctx.storage.put('buffer', []);
    // Alarm auto-deleted after success
  }
}

API 方法:

  • await ctx.storage.setAlarm(Date.now() + 60000) - 设置闹钟(覆盖现有闹钟)
  • await ctx.storage.getAlarm() - 获取时间戳或 null
  • await ctx.storage.deleteAlarm() - 取消闹钟
  • async alarm(info) - 闹钟触发时调用的处理程序

行为:

  • ✅ 至少执行一次,自动重试(最多 6 次,采用指数退避)
  • ✅ 休眠或被驱逐后仍会保留
  • ✅ 成功后自动删除
  • ⚠️ 每个 DO 只能有一个闹钟(新闹钟会覆盖现有闹钟)

RPC 与 HTTP Fetch

**RPC(推荐):**直接调用方法、类型安全、简单

// DO class
export class Counter extends DurableObject {
  async increment(): Promise<number> {
    let value = (await this.ctx.storage.get<number>('count')) || 0;
    await this.ctx.storage.put('count', ++value);
    return value;
  }
}

// Worker calls
const stub = env.COUNTER.getByName('my-counter');
const count = await stub.increment();  // Type-safe!

**HTTP Fetch:**请求/响应模式,WebSocket 升级时必须使用

// DO class
export class Counter extends DurableObject {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === '/increment') {
      let value = (await this.ctx.storage.get<number>('count')) || 0;
      await this.ctx.storage.put('count', ++value);
      return new Response(JSON.stringify({ count: value }));
    }
    return new Response('Not found', { status: 404 });
  }
}

// Worker calls
const stub = env.COUNTER.getByName('my-counter');
const response = await stub.fetch('https://fake-host/increment', { method: 'POST' });
const data = await response.json();

**使用场景:**新项目使用 RPC(更简单);WebSocket 升级或复杂路由使用 HTTP Fetch


获取 DO 存根

获取 ID 的三种方式:

  1. idFromName(name) - 一致路由(名称相同 = DO 相同)
const stub = env.CHAT_ROOM.getByName('room-123');  // Aug 2025: Shortcut for idFromName + get
// Use for: chat rooms, user sessions, per-tenant logic, singletons
  1. newUniqueId() - 随机唯一 ID(必须存储以供重用)
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });  // Optional: EU compliance
const idString = id.toString();  // Save to KV/D1 for later
  1. idFromString(idString) - 从已保存的 ID 重新创建
const id = env.MY_DO.idFromString(await env.KV.get('session:123'));
const stub = env.MY_DO.get(id);

位置提示(尽力而为):

const stub = env.MY_DO.get(id, { locationHint: 'enam' });  // wnam, enam, sam, weur, eeur, apac, oc, afr, me

管辖区域(严格执行):

const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });  // Options: 'eu', 'fedramp'
// Cannot combine with location hints, higher latency outside jurisdiction

迁移

**适用于:**创建、重命名、删除、转移 DO 类

1. 创建:

{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] }  // SQLite 10GB
// Or: "new_classes": ["Counter"]  // KV 128MB (legacy)

2. 重命名:

{ "migrations": [
  { "tag": "v1", "new_sqlite_classes": ["OldName"] },
  { "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] }
]}

3. 删除:

{ "migrations": [
  { "tag": "v1", "new_sqlite_classes": ["Counter"] },
  { "tag": "v2", "deleted_classes": ["Counter"] }  // Immediate deletion, cannot undo
]}

4. 转移:

{ "migrations": [{ "tag": "v1", "transferred_classes": [
  { "from": "OldClass", "from_script": "old-worker", "to": "NewClass" }
]}]}

迁移规则:

  • ❌ 原子执行(所有实例同时迁移,不支持逐步发布)
  • ❌ 标签必须唯一,且只能追加
  • ❌ 不能为现有的 KV 后端 DO 启用 SQLite
  • ✅ 代码更改无需迁移(只有架构更改需要)
  • ✅ 类名在每个账户内全局唯一

常见模式

速率限制:

async checkLimit(userId: string, limit: number, window: number): Promise<boolean> {
  const requests = (await this.ctx.storage.get<number[]>(`rate:${userId}`)) || [];
  const valid = requests.filter(t => Date.now() - t < window);
  if (valid.length >= limit) return false;
  valid.push(Date.now());
  await this.ctx.storage.put(`rate:${userId}`, valid);
  return true;
}

带 TTL 的会话管理:

async set(key: string, value: any, ttl?: number): Promise<void> {
  const expiresAt = ttl ? Date.now() + ttl : null;
  this.sql.exec('INSERT OR REPLACE INTO session (key, value, expires_at) VALUES (?, ?, ?)',
    key, JSON.stringify(value), expiresAt);
}

async alarm(): Promise<void> {
  this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now());
  await this.ctx.storage.setAlarm(Date.now() + 3600000);  // Hourly cleanup
}

领导者选举:

async electLeader(workerId: string): Promise<boolean> {
  try {
    this.sql.exec('INSERT INTO leader (id, worker_id, elected_at) VALUES (1, ?, ?)', workerId, Date.now());
    return true;
  } catch { return false; }  // Already has leader
}

多 DO 协调:

// Coordinator delegates to child DOs
const gameRoom = env.GAME_ROOM.getByName(gameId);
await gameRoom.initialize();
await this.ctx.storage.put(`game:${gameId}`, { created: Date.now() });

关键规则

始终执行

✅ 从 Worker 导出 DO 类

export class MyDO extends DurableObject { }
export default MyDO;  // Required

✅ 在构造函数中调用 super(ctx, env)

constructor(ctx: DurableObjectState, env: Env) {
  super(ctx, env);  // Required first line
}

✅ 新 DO 使用 new_sqlite_classes

{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }

✅ 使用 ctx.acceptWebSocket() 实现休眠

this.ctx.acceptWebSocket(server);  // Enables hibernation

✅ 将关键状态持久化到存储中(不要只保存在内存中)

await this.ctx.storage.put('important', value);

✅ 使用闹钟代替 setTimeout/setInterval

await this.ctx.storage.setAlarm(Date.now() + 60000);

✅ 使用参数化 SQL 查询

this.sql.exec('SELECT * FROM table WHERE id = ?', id);

✅ 尽量减少构造函数中的工作

constructor(ctx, env) {
  super(ctx, env);
  // Minimal initialization only
  ctx.blockConcurrencyWhile(async () => {
    // Load from storage
  });
}
绝不执行

❌ 不添加迁移就创建 DO

// Missing migrations array = error

❌ 忘记导出 DO 类

class MyDO extends DurableObject { }
// Missing: export default MyDO;

❌ 使用 setTimeout 或 setInterval

setTimeout(() => {}, 1000);  // Prevents hibernation

❌ 使用 WebSocket 时只依赖内存状态

// ❌ WRONG: this.sessions will be lost on hibernation
// ✅ CORRECT: Use serializeAttachment()

❌ 逐步部署迁移

# Migrations are atomic - cannot use gradual rollout

❌ 为现有的 KV 后端 DO 启用 SQLite

// Not supported - must create new DO class instead

❌ 期望标准 WebSocket API 支持休眠

ws.accept();  // ❌ No hibernation
this.ctx.acceptWebSocket(ws);  // ✅ Hibernation enabled

❌ 认为位置提示有保证

// Location hints are best-effort only

防止已知问题

此技能可防止 20 个已记录的问题:

问题 #1:未导出类

错误:"binding not found" 或 "Class X not found" 来源:https://developers.cloudflare.com/durable-objects/get-started/ 发生原因:未从 Worker 导出 DO 类 预防措施:

export class MyDO extends DurableObject { }
export default MyDO;  // ← Required
问题 #2:缺少迁移

错误:"migrations required" 或 "no migration found for class" 来源:https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/ 发生原因:创建 DO 类时未添加迁移条目 预防措施:创建新的 DO 类时始终添加迁移

{
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["MyDO"] }
  ]
}
问题 #3:迁移类型错误(KV 与 SQLite)

错误:架构错误、存储 API 不匹配 来源:https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/ 发生原因:使用了 new_classes 而不是 new_sqlite_classes 预防措施:SQLite 后端使用 new_sqlite_classes(推荐)

问题 #4:构造函数开销阻碍休眠唤醒

错误:休眠唤醒时间过长 来源:https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/ 原因:构造函数中执行了繁重操作 预防:尽量精简构造函数,使用 blockConcurrencyWhile()

constructor(ctx, env) {
  super(ctx, env);
  ctx.blockConcurrencyWhile(async () => {
    // Load from storage
  });
}
问题 #5:setTimeout 会破坏休眠

错误:DO 始终无法休眠,持续产生高额时长费用 来源:https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ 原因:setTimeout/setInterval 会阻止休眠 预防:改用 alarms API

// ❌ WRONG
setTimeout(() => {}, 1000);

// ✅ CORRECT
await this.ctx.storage.setAlarm(Date.now() + 1000);
问题 #6:休眠时内存状态丢失

错误:WebSocket 元数据丢失,状态意外重置 来源:https://developers.cloudflare.com/durable-objects/best-practices/websockets/ 原因:依赖了休眠时会被清除的内存状态 预防:使用 serializeAttachment() 保存 WebSocket 元数据

ws.serializeAttachment({ userId, username });

// Restore in constructor
ctx.getWebSockets().forEach(ws => {
  const metadata = ws.deserializeAttachment();
  this.sessions.set(ws, metadata);
});
问题 #7:传出的 WebSocket 无法休眠

错误:即使使用休眠 API,费用仍然很高 来源:Cloudflare 文档 | GitHub 问题 #4864 原因:Durable Objects 使用 new WebSocket('url') 与外部 WebSocket 服务保持持久连接时,无法休眠,会无限期地驻留在内存中 受影响的使用场景:

  • 实时数据库订阅(Supabase、Firebase)
  • 消息代理(Redis Streams、Apache Kafka)
  • 与外部实时服务建立 WebSocket 连接
  • 服务间通信 预防:仅对服务端(传入)的 WebSocket 使用 ctx.acceptWebSocket() 进行休眠。使用 new WebSocket(url) 创建的传出 WebSocket 连接会阻止休眠。如果需要休眠,请重新设计架构,避免 Durable Objects 建立传出 WebSocket 连接。
问题 #8:全局唯一性认知混淆

错误:DO 类名冲突出乎意料 来源:https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness 原因:每个账户中的 DO 类名全局唯一 预防:了解同一账户中的所有 Worker 共享 DO 类名

问题 #9:deleteAll 问题

错误:存储未完全删除,账单仍在产生;或闹钟处理程序中发生内部错误 来源:KV 存储 API | GitHub 问题 #2993 原因:

  • KV 后端的 deleteAll() 可能部分失败(非原子操作)
  • SQLite:在闹钟处理程序中调用 deleteAll() 会导致内部错误和重试循环(已在运行时中修复) 预防:
  • 使用 SQLite 后端以实现原子性 deleteAll
  • 在闹钟处理程序中,先调用 deleteAlarm(),再调用 deleteAll():
async alarm(info: { retryCount: number }): Promise<void> {
  await this.ctx.storage.deleteAlarm();  // ← Call first
  await this.ctx.storage.deleteAll();    // Then delete all
}
问题 #10:绑定名称不匹配

错误:访问 DO 绑定时发生运行时错误 来源:https://developers.cloudflare.com/durable-objects/get-started/ 原因:wrangler.jsonc 中的绑定名称与代码不匹配 预防:确保名称一致

{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }
env.MY_DO.getByName('instance');  // Must match binding name
问题 #11:超出状态大小限制

错误:出现 "state limit exceeded" 或存储错误 来源:https://developers.cloudflare.com/durable-objects/platform/pricing/ 原因:超出 1GB(SQLite)或 128MB(KV)的限制 预防:监控存储大小,并使用闹钟实现清理

问题 #12:迁移不是原子操作

错误:渐进式部署受阻 来源:https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ 原因:尝试在迁移期间使用渐进式发布 预防:迁移会在所有实例中原子式部署

问题 #13:忽略位置提示

错误:DO 创建在错误的区域 来源:https://developers.cloudflare.com/durable-objects/reference/data-location/ 发生原因:位置提示仅尽力而为,并非保证 预防措施:有严格要求时使用 jurisdiction

问题 #14:闹钟重试失败

错误:闹钟失败后任务丢失 来源:https://developers.cloudflare.com/durable-objects/api/alarms/ 发生原因:闹钟处理程序反复抛出错误 预防措施:实现幂等的闹钟处理程序

async alarm(info: { retryCount: number }): Promise<void> {
  if (info.retryCount > 3) {
    console.error('Giving up after 3 retries');
    return;
  }
  // Idempotent operation
}
问题 #15:Fetch 阻止休眠

错误:即使使用休眠 API,DO 也从不休眠 来源:https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ 发生原因:正在进行的 fetch() 请求会阻止休眠 预防措施:确保所有异步 I/O 都在空闲期开始前完成

问题 #16:SQLite 中的布尔值绑定为字符串

错误:布尔列包含字符串 "true"/"false",而不是整数 0/1;使用布尔值进行比较的 SQL 查询会失败 来源:GitHub Issue #9964 发生原因:JavaScript 布尔值在 Durable Objects SQLite 中会被序列化为字符串(与 D1 的行为不一致) 预防措施:手动将布尔值转换为整数,并使用 STRICT 表

// Convert booleans to integers
this.sql.exec('INSERT INTO test (bool_col) VALUES (?)', value ? 1 : 0);

// Use STRICT tables to catch type mismatches early
this.sql.exec(`
  CREATE TABLE IF NOT EXISTS test (
    id INTEGER PRIMARY KEY,
    bool_col INTEGER NOT NULL
  ) STRICT;
`);
问题 #17:RPC ReadableStream 取消操作会记录虚假的网络错误

错误:取消 RPC 返回的 ReadableStream 时,Wrangler dev 日志会显示“Network connection lost”,尽管取消操作正确完成 来源:GitHub Issue #11071 发生原因:通过 RPC 取消 Durable Object 返回的 ReadableStream,会在 Wrangler dev 中触发误导性错误日志(这是显示问题,并非运行时错误) 预防措施:目前没有可用的变通方法。取消操作会正确完成,可忽略 Wrangler dev 中的虚假错误日志。此问题不会出现在生产环境或仅使用 workerd 的配置中。

问题 #18:blockConcurrencyWhile 在本地开发中不会阻塞(已修复)

错误:构造函数中的 blockConcurrencyWhile 在本地开发时不会阻塞请求,导致开发期间被隐藏的竞态条件 来源:GitHub Issue #8686 发生原因:较旧版本的 @cloudflare/vite-plugin 和 wrangler 中存在 Bug 预防措施:升级到 @cloudflare/vite-plugin v1.3.1+ 和 wrangler v4.18.0+,这些版本已修复此问题

问题 #19:不支持在多个 wrangler dev 会话之间使用 RPC

错误:"Cannot access MyDurableObject#myMethod as Durable Object RPC is not yet supported between multiple wrangler dev sessions" 来源:GitHub Issue #11944 发生原因:在本地开发中,尚不支持从多个 wrangler dev 实例(例如,monorepo 中的独立 Workers)通过 RPC 访问 Durable Object 预防措施:使用 wrangler dev -c config1 -c config2 在单个会话中运行多个 Worker,或者在本地开发期间使用 HTTP fetch 而非 RPC 进行跨 Worker 的 DO 通信

问题 #20:构造函数中的 state.id.name 未定义(vitest 回归问题)

错误:使用 @cloudflare/vitest-pool-workers 0.8.71 时,构造函数中的 DurableObjectState.id.name 未定义 来源:GitHub Issue #11580 发生原因:vitest-pool-workers 0.8.71 中的回归问题(在 0.8.38 中正常工作) 预防措施:降级到 @cloudflare/[email protected],或升级到已修复此问题的更高版本


配置与类型

wrangler.jsonc:

{
  "compatibility_date": "2025-11-23",
  "durable_objects": {
    "bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Counter"] },
    { "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterV2" }] }
  ]
}

TypeScript:

import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';

interface Env { MY_DO: DurableObjectNamespace<MyDurableObject>; }

export class MyDurableObject extends DurableObject<Env> {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;
  }
}

官方文档


有问题?遇到故障?

  1. 查看 references/top-errors.md,了解常见问题
  2. 查看 templates/ 中的可用示例
  3. 参阅官方文档:https://developers.cloudflare.com/durable-objects/
  4. 仔细核对迁移配置

最后验证时间:2026-01-21 | 技能版本:3.1.0 | 变更:新增 5 个问题(布尔绑定、RPC 流取消、blockConcurrencyWhile 本地开发、RPC 多会话、vitest 回归),扩展问题 #7(传出 WebSocket 的用例)和问题 #9(deleteAll 与闹钟的交互),新增 STRICT 表最佳实践,更新 @cloudflare/actors beta 警告

评论

支持 Markdown