cloudflare-durable-objects by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-durable-objects状态 : 生产就绪 ✅ 最后更新 : 2026-01-21 依赖项 : cloudflare-worker-base (推荐) 最新版本 : wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0 官方文档 : https://developers.cloudflare.com/durable-objects/
近期更新 (2025) :
getByName() API 快捷方式搭建新的 DO 项目:
npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
或添加到现有 Worker:
// src/counter.ts - Durable Object 类
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; // 关键:必须导出
// wrangler.jsonc - 配置
{
"durable_objects": {
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] } // SQLite 后端 (10GB 限制)
]
}
// 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'); // 2025年8月:getByName() 快捷方式
return new Response(`计数: ${await stub.increment()}`);
}
};
import { DurableObject } from 'cloudflare:workers';
export class MyDO extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // 必需的第一行
// 在请求前加载状态(可选)
ctx.blockConcurrencyWhile(async () => {
this.value = await ctx.storage.get('key') || defaultValue;
});
}
// RPC 方法(推荐)
async myMethod(): Promise<string> { return 'Hello'; }
// HTTP fetch 处理器(可选)
async fetch(request: Request): Promise<Response> { return new Response('OK'); }
}
export default MyDO; // 关键:必须导出
// Worker 也必须导出 DO 类
import { MyDO } from './my-do';
export { MyDO };
构造函数规则:
super(ctx, env)ctx.blockConcurrencyWhile() 进行存储初始化setTimeout/setInterval(使用警报)提供两种后端:
在迁移中启用 SQLite:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }] }
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; // 2025年2月:查询性能优化
`);
}
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 optimizeSTRICT 关键字以强制类型亲和性并及早捕获类型不匹配// 单操作
await this.ctx.storage.put('key', value);
const value = await this.ctx.storage.get<T>('key');
await this.ctx.storage.delete('key');
// 批量操作
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']);
// 列出和删除所有
const map = await this.ctx.storage.list({ prefix: 'user:', limit: 100 });
await this.ctx.storage.deleteAll(); // 仅在 SQLite 上是原子的
// 事务
await this.ctx.storage.transaction(async (txn) => {
await txn.put('key1', val1);
await txn.put('key2', val2);
});
存储限制: SQLite 10GB (2025年4月 GA) | KV 128MB
功能:
工作原理:
关键: 内存状态在休眠时会丢失。使用 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();
// 关键:休眠后恢复 WebSocket 元数据
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') };
// 关键:使用 ctx.acceptWebSocket(),而不是 ws.accept()
this.ctx.acceptWebSocket(server);
server.serializeAttachment(metadata); // 在休眠期间持久化
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);
// 处理消息(自 2025年10月起最大 32 MiB)
}
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/setIntervalws.accept() - 标准 API,不支持休眠setTimeout/setInterval - 阻止休眠fetch() - 阻止休眠安排 DO 在未来某个时间唤醒。用于: 批处理、清理、提醒、周期性任务。
export class Batcher extends DurableObject {
async addItem(item: string): Promise<void> {
// 添加到缓冲区
const buffer = await this.ctx.storage.get<string[]>('buffer') || [];
buffer.push(item);
await this.ctx.storage.put('buffer', buffer);
// 如果未设置则安排警报
if ((await this.ctx.storage.getAlarm()) === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000); // 10 秒后
}
}
async alarm(info: { retryCount: number; isRetry: boolean }): Promise<void> {
if (info.retryCount > 3) return; // 重试 3 次后放弃
const buffer = await this.ctx.storage.get<string[]>('buffer') || [];
await this.processBatch(buffer);
await this.ctx.storage.put('buffer', []);
// 成功后警报自动删除
}
}
API 方法:
await ctx.storage.setAlarm(Date.now() + 60000) - 设置警报(覆盖现有)await ctx.storage.getAlarm() - 获取时间戳或 nullawait ctx.storage.deleteAlarm() - 取消警报async alarm(info) - 警报触发时调用的处理器行为:
RPC (推荐): 直接方法调用,类型安全,简单
// DO 类
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 调用
const stub = env.COUNTER.getByName('my-counter');
const count = await stub.increment(); // 类型安全!
HTTP Fetch: 请求/响应模式,WebSocket 升级必需
// DO 类
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 调用
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
获取 ID 的三种方式:
idFromName(name) - 一致性路由(相同名称 = 相同 DO)const stub = env.CHAT_ROOM.getByName('room-123'); // 2025年8月:idFromName + get 的快捷方式
// 用于:聊天室、用户会话、租户逻辑、单例
2. newUniqueId() - 随机唯一 ID(必须存储以供重用)
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // 可选:欧盟合规性
const idString = id.toString(); // 保存到 KV/D1 以备后用
3. 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' }); // 选项:'eu', 'fedramp'
// 不能与位置提示结合使用,管辖区域外延迟更高
需要迁移的情况: 创建、重命名、删除、转移 DO 类
1. 创建:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] } // SQLite 10GB
// 或: "new_classes": ["Counter"] // KV 128MB (旧版)
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"] } // 立即删除,无法撤销
]}
4. 转移:
{ "migrations": [{ "tag": "v1", "transferred_classes": [
{ "from": "OldClass", "from_script": "old-worker", "to": "NewClass" }
]}]}
迁移规则:
速率限制:
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); // 每小时清理
}
领导者选举:
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; } // 已有领导者
}
多 DO 协调:
// 协调器委托给子 DO
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; // 必需
✅ 在构造函数中调用 super(ctx, env)
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // 必需的第一行
}
✅ 对新 DO 使用 new_sqlite_classes
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
✅ 使用 ctx.acceptWebSocket() 进行休眠
this.ctx.acceptWebSocket(server); // 启用休眠
✅ 将关键状态持久化到存储(不仅仅是内存)
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);
// 仅进行最小化初始化
ctx.blockConcurrencyWhile(async () => {
// 从存储加载
});
}
❌ 创建 DO 时不添加迁移
// 缺少 migrations 数组 = 错误
❌ 忘记导出 DO 类
class MyDO extends DurableObject { }
// 缺少: export default MyDO;
❌ 使用 setTimeout 或 setInterval
setTimeout(() => {}, 1000); // 阻止休眠
❌ 仅依赖 WebSocket 的内存状态
// ❌ 错误:this.sessions 在休眠时会丢失
// ✅ 正确:使用 serializeAttachment()
❌ 逐步部署迁移
# 迁移是原子的 - 不能使用逐步推出
❌ 在现有的 KV 后端 DO 上启用 SQLite
// 不支持 - 必须创建新的 DO 类
❌ 使用标准 WebSocket API 并期望休眠
ws.accept(); // ❌ 不支持休眠
this.ctx.acceptWebSocket(ws); // ✅ 启用休眠
❌ 假设位置提示是保证的
// 位置提示仅是尽力而为
此技能预防 20 个已记录的问题 :
错误 : "binding not found" 或 "Class X not found" 来源 : https://developers.cloudflare.com/durable-objects/get-started/ 发生原因 : DO 类未从 Worker 导出 预防措施 :
export class MyDO extends DurableObject { }
export default MyDO; // ← 必需
错误 : "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"] }
]
}
错误 : 模式错误,存储 API 不匹配 来源 : https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/ 发生原因 : 使用了 new_classes 而不是 new_sqlite_classes 预防措施 : 对 SQLite 后端使用 new_sqlite_classes(推荐)
错误 : 休眠唤醒时间慢 来源 : https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/ 发生原因 : 构造函数中工作繁重 预防措施 : 最小化构造函数,使用 blockConcurrencyWhile()
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// 从存储加载
});
}
错误 : DO 从不休眠,高持续时间费用 来源 : https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ 发生原因 : setTimeout/setInterval 阻止休眠 预防措施 : 改用警报 API
// ❌ 错误
setTimeout(() => {}, 1000);
// ✅ 正确
await this.ctx.storage.setAlarm(Date.now() + 1000);
错误 : WebSocket 元数据丢失,状态意外重置 来源 : https://developers.cloudflare.com/durable-objects/best-practices/websockets/ 发生原因 : 依赖了在休眠时被清除的内存状态 预防措施 : 使用 serializeAttachment() 处理 WebSocket 元数据
ws.serializeAttachment({ userId, username });
// 在构造函数中恢复
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
错误 : 尽管使用了休眠 API,费用仍然很高 来源 : Cloudflare 文档 | GitHub Issue #4864 发生原因 : 使用 new WebSocket('url') 与外部 WebSocket 服务保持持久连接的 Durable Objects 无法休眠,并无限期地固定在内存中 受影响的使用场景 :
ctx.acceptWebSocket() 建立的服务器端(入站)WebSocket 使用休眠。使用 new WebSocket(url) 创建的出站 WebSocket 连接会阻止休眠。如果需要休眠,请重新设计架构以避免从 Durable Objects 建立出站 WebSocket 连接。错误 : 意外的 DO 类名冲突 来源 : https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness 发生原因 : DO 类名在账户内全局唯一 预防措施 : 理解 DO 类名在账户内所有 Worker 之间共享
错误 : 存储未完全删除,计费继续;或警报处理器中出现内部错误 来源 : KV 存储 API | GitHub Issue #2993 发生原因 :
KV 后端的 deleteAll() 可能部分失败(非原子)
SQLite:在警报处理器中调用 deleteAll() 会导致内部错误和重试循环(运行时已修复) 预防措施 :
使用 SQLite 后端进行原子 deleteAll
在警报处理器中,在 deleteAll() 之前调用 deleteAlarm():
async alarm(info: { retryCount: number }): Promise<void> { await this.ctx.storage.deleteAlarm(); // ← 首先调用 await this.ctx.storage.deleteAll(); // 然后删除所有 }
错误 : 访问 DO 绑定时出现运行时错误 来源 : https://developers.cloudflare.com/durable-objects/get-started/ 发生原因 : wrangler.jsonc 中的绑定名称与代码不匹配 预防措施 : 确保一致性
{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }
env.MY_DO.getByName('instance'); // 必须匹配绑定名称
错误 : "state limit exceeded" 或存储错误 来源 : https://developers.cloudflare.com/durable-objects/platform/pricing/ 发生原因 : 超过 1GB (SQLite) 或 128MB (KV) 限制 预防措施 : 监控存储大小,使用警报实现清理
错误 : 逐步部署被阻止 来源 : https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ 发生原因 : 尝试对迁移使用逐步推出 预防措施 : 迁移在所有实例间原子性部署
错误 : DO 在错误区域创建 来源 : https://developers.cloudflare.com/durable-objects/reference/data-location/ 发生原因 : 位置提示是尽力而为,不保证 预防措施 : 对严格要求使用管辖区域
错误 : 警报失败后任务丢失 来源 : https://developers.cloudflare.com/durable-objects/api/alarms/ 发生原因 : 警报处理器重复抛出错误 预防措施 : 实现幂等的警报处理器
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('重试 3 次后放弃');
return;
}
// 幂等操作
}
错误 : 尽管使用了休眠 API,DO 从不休眠 来源 : https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ 发生原因 : 进行中的 fetch() 请求阻止休眠 预防措施 : 确保所有异步 I/O 在空闲期前完成
错误 : 布尔列包含字符串 "true"/"false" 而不是整数 0/1;使用布尔比较的 SQL 查询失败 来源 : GitHub Issue #9964 发生原因 : JavaScript 布尔值在 Durable Objects SQLite 中被序列化为字符串(与 D1 行为不一致) 预防措施 : 手动将布尔值转换为整数并使用 STRICT 表
// 将布尔值转换为整数
this.sql.exec('INSERT INTO test (bool_col) VALUES (?)', value ? 1 : 0);
// 使用 STRICT 表及早捕获类型不匹配
this.sql.exec(`
CREATE TABLE IF NOT EXISTS test (
id INTEGER PRIMARY KEY,
bool_col INTEGER NOT NULL
) STRICT;
`);
错误 : 取消来自 RPC 的 ReadableStream 时,Wrangler dev 日志显示“网络连接丢失”,尽管取消正确 来源 : GitHub Issue #11071 发生原因 : 取消通过 RPC 从 Durable Object 返回的 ReadableStream 会在 Wrangler dev 中触发误导性错误日志(表示问题,非运行时错误) 预防措施 : 无可用解决方法。取消工作正常 - 忽略 Wrangler dev 中的虚假错误日志。此问题不会出现在生产环境或仅 workerd 的设置中。
错误 : 构造函数中的 blockConcurrencyWhile 在本地开发中不阻塞请求,导致开发期间隐藏的竞态条件 来源 : GitHub Issue #8686 发生原因 : 旧版 @cloudflare/vite-plugin 和 wrangler 版本中的错误 预防措施 : 升级到 @cloudflare/vite-plugin v1.3.1+ 和 wrangler v4.18.0+,此问题已修复
错误 : "Cannot access MyDurableObject#myMethod as Durable Object RPC is not yet supported between multiple wrangler dev sessions" 来源 : GitHub Issue #11944 发生原因 : 从多个 wrangler dev 实例(例如,monorepo 中的独立 Worker)通过 RPC 访问 Durable Object 在本地开发中尚不支持 预防措施 : 使用 wrangler dev -c config1 -c config2 在单个会话中运行多个 worker,或者在本地开发期间对跨 worker 的 DO 通信使用 HTTP fetch 代替 RPC
错误 : 使用 @cloudflare/vitest-pool-workers 0.8.71 时,DurableObjectState.id.name 在构造函数中为 undefined 来源 : GitHub Issue #11580 发生原因 : vitest-pool-workers 0.8.71 中的回归(在 0.8.38 中工作正常) 预防措施 : 降级到 @cloudflare/vitest-pool-workers@0.8.38 或升级到修复此问题的后续版本
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;
}
}
有问题?有疑问?
references/top-errors.md 了解常见问题templates/ 获取工作示例最后验证 : 2026-01-21 | 技能版本 : 3.1.0 | 变更 : 新增 5 个问题(布尔值绑定、RPC 流取消、blockConcurrencyWhile 本地开发、RPC 多会话、vitest 回归),扩展了问题 #7(出站 WebSocket 使用场景)和问题 #9(deleteAll 与警报交互),添加了 STRICT 表最佳实践,更新了 @cloudflare/actors beta 警告
每周安装数
332
仓库
GitHub 星标数
643
首次出现
2026年1月20日
安全
Status : Production Ready ✅ Last Updated : 2026-01-21 Dependencies : cloudflare-worker-base (recommended) Latest Versions : wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0 Official Docs : https://developers.cloudflare.com/durable-objects/
Recent Updates (2025) :
getByName() API shortcut for named DOsScaffold new DO project:
npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts
Or add to existing 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()}`);
}
};
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 };
Constructor Rules:
super(ctx, env) firstctx.blockConcurrencyWhile() for storage initializationsetTimeout/setInterval (use alarms)Two backends available:
Enable SQLite in migrations:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }] }
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 Methods:
sql.exec(query, ...params) → cursorcursor.one<T>() → single row (throws if none)cursor.one<T>({ allowNone: true }) → row or nullcursor.toArray<T>() → all rowsctx.storage.transactionSync(() => { ... }) → atomic multi-statementBest Practices:
? placeholders for parameterized queriesPRAGMA optimize after schema changesSTRICT keyword to table definitions to enforce type affinity and catch type mismatches early// 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);
});
Storage Limits: SQLite 10GB (April 2025 GA) | KV 128MB
Capabilities:
How it works:
CRITICAL: In-memory state is lost on hibernation. Use serializeAttachment() to persist per-WebSocket metadata.
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);
}
}
Hibernation Rules:
ctx.acceptWebSocket(ws) - enables hibernationws.serializeAttachment(data) - persist metadatactx.getWebSockets().forEach() - restore in constructorsetTimeout/setIntervalws.accept() - standard API, no hibernationsetTimeout/setInterval - prevents hibernationfetch() - blocks hibernationSchedule DO to wake at future time. Use for: batching, cleanup, reminders, periodic tasks.
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 Methods:
await ctx.storage.setAlarm(Date.now() + 60000) - set alarm (overwrites existing)await ctx.storage.getAlarm() - get timestamp or nullawait ctx.storage.deleteAlarm() - cancel alarmasync alarm(info) - handler called when alarm firesBehavior:
RPC (Recommended): Direct method calls, type-safe, simple
// 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: Request/response pattern, required for WebSocket upgrades
// 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();
When to use: RPC for new projects (simpler), HTTP Fetch for WebSocket upgrades or complex routing
Three ways to get IDs:
idFromName(name) - Consistent routing (same name = same 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
2. newUniqueId() - Random unique ID (must store for reuse)
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Optional: EU compliance
const idString = id.toString(); // Save to KV/D1 for later
3. idFromString(idString) - Recreate from saved ID
const id = env.MY_DO.idFromString(await env.KV.get('session:123'));
const stub = env.MY_DO.get(id);
Location hints (best-effort):
const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // wnam, enam, sam, weur, eeur, apac, oc, afr, me
Jurisdiction (strict enforcement):
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Options: 'eu', 'fedramp'
// Cannot combine with location hints, higher latency outside jurisdiction
Required for: create, rename, delete, transfer DO classes
1. Create:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] } // SQLite 10GB
// Or: "new_classes": ["Counter"] // KV 128MB (legacy)
2. Rename:
{ "migrations": [
{ "tag": "v1", "new_sqlite_classes": ["OldName"] },
{ "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] }
]}
3. Delete:
{ "migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] },
{ "tag": "v2", "deleted_classes": ["Counter"] } // Immediate deletion, cannot undo
]}
4. Transfer:
{ "migrations": [{ "tag": "v1", "transferred_classes": [
{ "from": "OldClass", "from_script": "old-worker", "to": "NewClass" }
]}]}
Migration Rules:
Rate Limiting:
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;
}
Session Management with 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
}
Leader Election:
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
}
Multi-DO Coordination:
// 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() });
✅ Export DO class from Worker
export class MyDO extends DurableObject { }
export default MyDO; // Required
✅ Callsuper(ctx, env) in constructor
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // Required first line
}
✅ Usenew_sqlite_classes for new DOs
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
✅ Usectx.acceptWebSocket() for hibernation
this.ctx.acceptWebSocket(server); // Enables hibernation
✅ Persist critical state to storage (not just memory)
await this.ctx.storage.put('important', value);
✅ Use alarms instead of setTimeout/setInterval
await this.ctx.storage.setAlarm(Date.now() + 60000);
✅ Use parameterized SQL queries
this.sql.exec('SELECT * FROM table WHERE id = ?', id);
✅ Minimize constructor work
constructor(ctx, env) {
super(ctx, env);
// Minimal initialization only
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}
❌ Create DO without migration
// Missing migrations array = error
❌ Forget to export DO class
class MyDO extends DurableObject { }
// Missing: export default MyDO;
❌ UsesetTimeout or setInterval
setTimeout(() => {}, 1000); // Prevents hibernation
❌ Rely only on in-memory state with WebSockets
// ❌ WRONG: this.sessions will be lost on hibernation
// ✅ CORRECT: Use serializeAttachment()
❌ Deploy migrations gradually
# Migrations are atomic - cannot use gradual rollout
❌ Enable SQLite on existing KV-backed DO
// Not supported - must create new DO class instead
❌ Use standard WebSocket API expecting hibernation
ws.accept(); // ❌ No hibernation
this.ctx.acceptWebSocket(ws); // ✅ Hibernation enabled
❌ Assume location hints are guaranteed
// Location hints are best-effort only
This skill prevents 20 documented issues :
Error : "binding not found" or "Class X not found" Source : https://developers.cloudflare.com/durable-objects/get-started/ Why It Happens : DO class not exported from Worker Prevention :
export class MyDO extends DurableObject { }
export default MyDO; // ← Required
Error : "migrations required" or "no migration found for class" Source : https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/ Why It Happens : Created DO class without migration entry Prevention : Always add migration when creating new DO class
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
]
}
Error : Schema errors, storage API mismatch Source : https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/ Why It Happens : Used new_classes instead of new_sqlite_classes Prevention : Use new_sqlite_classes for SQLite backend (recommended)
Error : Slow hibernation wake-up times Source : https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/ Why It Happens : Heavy work in constructor Prevention : Minimize constructor, use blockConcurrencyWhile()
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}
Error : DO never hibernates, high duration charges Source : https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ Why It Happens : setTimeout/setInterval prevents hibernation Prevention : Use alarms API instead
// ❌ WRONG
setTimeout(() => {}, 1000);
// ✅ CORRECT
await this.ctx.storage.setAlarm(Date.now() + 1000);
Error : WebSocket metadata lost, state reset unexpectedly Source : https://developers.cloudflare.com/durable-objects/best-practices/websockets/ Why It Happens : Relied on in-memory state that's cleared on hibernation Prevention : Use serializeAttachment() for WebSocket metadata
ws.serializeAttachment({ userId, username });
// Restore in constructor
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
Error : High charges despite hibernation API Source : Cloudflare Docs | GitHub Issue #4864 Why It Happens : Durable Objects maintaining persistent connections to external WebSocket services using new WebSocket('url') cannot hibernate and remain pinned in memory indefinitely Use Cases Affected :
ctx.acceptWebSocket(). Outgoing WebSocket connections created with new WebSocket(url) prevent hibernation. Redesign architecture to avoid outgoing WebSocket connections from Durable Objects if hibernation is required.Error : Unexpected DO class name conflicts Source : https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness Why It Happens : DO class names are globally unique per account Prevention : Understand DO class names are shared across all Workers in account
Error : Storage not fully deleted, billing continues; or internal error in alarm handler Source : KV Storage API | GitHub Issue #2993 Why It Happens :
KV backend deleteAll() can fail partially (not atomic)
SQLite: calling deleteAll() in alarm handler causes internal error and retry loop (fixed in runtime) Prevention :
Use SQLite backend for atomic deleteAll
In alarm handlers, call deleteAlarm() BEFORE deleteAll():
async alarm(info: { retryCount: number }): Promise<void> { await this.ctx.storage.deleteAlarm(); // ← Call first await this.ctx.storage.deleteAll(); // Then delete all }
Error : Runtime error accessing DO binding Source : https://developers.cloudflare.com/durable-objects/get-started/ Why It Happens : Binding name in wrangler.jsonc doesn't match code Prevention : Ensure consistency
{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }
env.MY_DO.getByName('instance'); // Must match binding name
Error : "state limit exceeded" or storage errors Source : https://developers.cloudflare.com/durable-objects/platform/pricing/ Why It Happens : Exceeded 1GB (SQLite) or 128MB (KV) limit Prevention : Monitor storage size, implement cleanup with alarms
Error : Gradual deployment blocked Source : https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ Why It Happens : Tried to use gradual rollout with migrations Prevention : Migrations deploy atomically across all instances
Error : DO created in wrong region Source : https://developers.cloudflare.com/durable-objects/reference/data-location/ Why It Happens : Location hints are best-effort, not guaranteed Prevention : Use jurisdiction for strict requirements
Error : Tasks lost after alarm failures Source : https://developers.cloudflare.com/durable-objects/api/alarms/ Why It Happens : Alarm handler throws errors repeatedly Prevention : Implement idempotent alarm handlers
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
return;
}
// Idempotent operation
}
Error : DO never hibernates despite using hibernation API Source : https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/ Why It Happens : In-progress fetch() requests prevent hibernation Prevention : Ensure all async I/O completes before idle period
Error : Boolean columns contain strings "true"/"false" instead of integers 0/1; SQL queries with boolean comparisons fail Source : GitHub Issue #9964 Why It Happens : JavaScript boolean values are serialized as strings in Durable Objects SQLite (inconsistent with D1 behavior) Prevention : Manually convert booleans to integers and use STRICT tables
// 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;
`);
Error : Wrangler dev logs show "Network connection lost" when canceling ReadableStream from RPC, despite correct cancellation Source : GitHub Issue #11071 Why It Happens : Canceling ReadableStream returned from Durable Object via RPC triggers misleading error logs in Wrangler dev (presentation issue, not runtime bug) Prevention : No workaround available. The cancellation works correctly - ignore the false error logs in Wrangler dev. Issue does not appear in production or workerd-only setup.
Error : Constructor's blockConcurrencyWhile doesn't block requests in local dev, causing race conditions hidden during development Source : GitHub Issue #8686 Why It Happens : Bug in older @cloudflare/vite-plugin and wrangler versions Prevention : Upgrade to @cloudflare/vite-plugin v1.3.1+ and wrangler v4.18.0+ where this is fixed
Error : "Cannot access MyDurableObject#myMethod as Durable Object RPC is not yet supported between multiple wrangler dev sessions" Source : GitHub Issue #11944 Why It Happens : Accessing a Durable Object over RPC from multiple wrangler dev instances (e.g., separate Workers in monorepo) is not yet supported in local dev Prevention : Use wrangler dev -c config1 -c config2 to run multiple workers in single session, or use HTTP fetch instead of RPC for cross-worker DO communication during local development
Error : DurableObjectState.id.name is undefined in constructor when using @cloudflare/vitest-pool-workers 0.8.71 Source : GitHub Issue #11580 Why It Happens : Regression in vitest-pool-workers 0.8.71 (worked in 0.8.38) Prevention : Downgrade to @cloudflare/vitest-pool-workers@0.8.38 or upgrade to later version where this is fixed
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;
}
}
Questions? Issues?
references/top-errors.md for common problemstemplates/ for working examplesLast verified : 2026-01-21 | Skill version : 3.1.0 | Changes : Added 5 new issues (boolean binding, RPC stream cancel, blockConcurrencyWhile local dev, RPC multi-session, vitest regression), expanded Issue #7 (outgoing WebSocket use cases) and Issue #9 (deleteAll alarm interaction), added STRICT tables best practice, updated @cloudflare/actors beta warning
Weekly Installs
332
Repository
GitHub Stars
643
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
claude-code273
gemini-cli223
opencode218
cursor208
antigravity197
codex193
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装