关于 cloudflare-durable-objects
用于在 Workers 中构建 Cloudflare Durable Objects 的实用技能,讲解存储、WebSocket 休眠、闹钟、RPC、迁移及常见实现模式。
主要功能
- 01配置 SQLite 或键值存储
- 02处理 WebSocket 休眠与状态
- 03使用闹钟安排持久任务
- 04采用 RPC 或 HTTP fetch 接口
- 05配置类和存储迁移
- 06应用限流与会话管理模式
适用场景
- 01构建持久化的 WebSocket 聊天室
- 02安排清理或批处理任务
- 03存储用户会话或限流数据
- 04协调多个 Durable Objects 的工作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ovachiever/ droid-tings --skill cloudflare-durable-objects安装后在 Claude Code 中输入/cloudflare-durable-objects确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonreferences/alarms-api.mdbest-practices.mdmigrations-guide.mdrpc-patterns.mdstate-api-reference.mdtop-errors.mdwebsocket-hibernation.mdwrangler-commands.mdscripts/check-versions.shtemplates/alarms-api-do.tsbasic-do.tslocation-hints.tsmulti-do-coordination.tspackage.jsonrpc-vs-fetch.tsstate-api-patterns.tswebsocket-hibernation-do.tswrangler-do-config.jsonc详细介绍
Cloudflare Durable Objects
状态:已可用于生产环境 ✅ 最后更新:2025-11-23 依赖项: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 库(Beta)——推荐的 SDK,支持迁移、闹钟和 Actor 类模式
- 2025 年 5 月:Python Workers 支持 Durable Objects
- 2025 年 4 月:SQLite 正式发布,存储空间提升至 10GB(从 Beta 转为正式版,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 })→ 单行或 nullcursor.toArray<T>()→ 所有行ctx.storage.transactionSync(() => { ... })→ 原子执行多条语句
**规则:**始终使用 ? 占位符,创建索引,并在架构变更后运行 PRAGMA optimize
键值 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 提升)
工作方式:
- 活跃 → 处理消息
- 空闲 → 约 10 秒无活动
- 休眠 → 内存状态被清除,WebSocket 保持连接
- 唤醒 → 收到消息 → 运行构造函数 → 调用处理程序
重要:休眠时会丢失内存状态。使用 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()——会阻止休眠
闹钟 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()——获取时间戳或 nullawait 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 的三种方式:
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
newUniqueId()- 随机唯一 ID(必须存储以便重复使用)
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Optional: EU compliance
const idString = id.toString(); // Save to KV/D1 for later
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
已知问题预防
此技能可预防 15+ 个有记录的问题:
问题 #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 会阻止休眠
预防方法:改用闹钟 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,费用仍然很高 来源:https://developers.cloudflare.com/durable-objects/best-practices/websockets/ 原因:外发 WebSocket 不支持休眠 预防措施:仅对服务端(传入)WebSocket 使用休眠
问题 #8:全局唯一性认知不清
错误:意外的 DO 类名称冲突 来源:https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness 原因:每个账户中的 DO 类名称都是全局唯一的 预防措施:了解 DO 类名称在账户中的所有 Worker 之间共享
问题 #9:KV 后端的 deleteAll 未完全删除
错误:存储未完全删除,费用仍在产生
来源:https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
原因:KV 后端的 deleteAll() 可能会部分失败
预防措施:使用 SQLite 后端实现原子性 deleteAll
问题 #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)的限制
预防措施:监控存储大小,并使用 alarms 实现清理
问题 #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:Alarm 重试失败
错误:Alarm 失败后任务丢失 来源:https://developers.cloudflare.com/durable-objects/api/alarms/ 原因:Alarm 处理程序反复抛出错误 预防措施:实现幂等的 Alarm 处理程序
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 都在空闲期开始前完成
配置与类型
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;
}
}
官方文档
- Durable Objects:https://developers.cloudflare.com/durable-objects/
- State API(SQL):https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
- WebSocket 休眠:https://developers.cloudflare.com/durable-objects/best-practices/websockets/
- Alarms API:https://developers.cloudflare.com/durable-objects/api/alarms/
- 迁移:https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
- 最佳实践:https://developers.cloudflare.com/durable-objects/best-practices/
- 定价:https://developers.cloudflare.com/durable-objects/platform/pricing/
有问题?遇到故障?
- 查看
references/top-errors.md,了解常见问题 - 查看
templates/中的可用示例 - 查阅官方文档:https://developers.cloudflare.com/durable-objects/
- 仔细检查迁移配置