cloudflare-durable-objects
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCloudflare Durable Objects
Cloudflare Durable Objects
Status: Production Ready ✅
Last Updated: 2025-10-22
Dependencies: cloudflare-worker-base (recommended)
Latest Versions: wrangler@4.43.0+, @cloudflare/workers-types@4.20251014.0+
Official Docs: https://developers.cloudflare.com/durable-objects/
状态: 已就绪可用于生产环境 ✅
最后更新: 2025-10-22
依赖: cloudflare-worker-base(推荐)
最新版本: wrangler@4.43.0+, @cloudflare/workers-types@4.20251014.0+
官方文档: https://developers.cloudflare.com/durable-objects/
What are Durable Objects?
什么是Durable Objects?
Cloudflare Durable Objects are globally unique, stateful objects that provide:
- Single-point coordination - Each Durable Object instance is globally unique across Cloudflare's network
- Strong consistency - Transactional, serializable storage (ACID guarantees)
- Real-time communication - WebSocket Hibernation API for thousands of connections per instance
- Persistent state - Built-in SQLite database (up to 1GB) or key-value storage
- Scheduled tasks - Alarms API for future task execution
- Global distribution - Automatically routed to optimal location
- Automatic scaling - Millions of independent instances
Use Cases:
- Chat rooms and real-time collaboration
- Multiplayer game servers
- Rate limiting and session management
- Leader election and coordination
- WebSocket servers with hibernation
- Stateful workflows and queues
- Per-user or per-room logic
Cloudflare Durable Objects是全局唯一的有状态对象,提供以下能力:
- 单点协调 - 每个Durable Object实例在Cloudflare网络中全局唯一
- 强一致性 - 事务性、可序列化存储(ACID保证)
- 实时通信 - WebSocket休眠API,每个实例可处理数千个连接
- 持久化状态 - 内置SQLite数据库(最大1GB)或键值存储
- 定时任务 - Alarms API用于未来任务执行
- 全局分发 - 自动路由到最优位置
- 自动扩展 - 支持数百万个独立实例
适用场景:
- 聊天室和实时协作
- 多人游戏服务器
- 速率限制和会话管理
- 领导者选举与协调
- 支持休眠的WebSocket服务器
- 有状态工作流和队列
- 每个用户或每个房间的专属逻辑
Quick Start (10 Minutes)
快速入门(10分钟)
Option 1: Scaffold New DO Project
选项1:搭建新的DO项目
bash
npm create cloudflare@latest my-durable-app -- \
--template=cloudflare/durable-objects-template \
--ts \
--git \
--deploy false
cd my-durable-app
npm install
npm run devWhat this creates:
- Complete Durable Objects project structure
- TypeScript configuration
- wrangler.jsonc with bindings and migrations
- Example DO class implementation
- Worker to call the DO
bash
npm create cloudflare@latest my-durable-app -- \
--template=cloudflare/durable-objects-template \
--ts \
--git \
--deploy false
cd my-durable-app
npm install
npm run dev创建的内容:
- 完整的Durable Objects项目结构
- TypeScript配置
- 包含绑定和迁移的wrangler.jsonc
- 示例DO类实现
- 用于调用DO的Worker
Option 2: Add to Existing Worker
选项2:添加到现有Worker
bash
cd my-existing-worker
npm install -D @cloudflare/workers-typesCreate a Durable Object class ():
src/counter.tstypescript
import { DurableObject } from 'cloudflare:workers';
export class Counter extends DurableObject {
async increment(): Promise<number> {
// Get current value from storage (default to 0)
let value: number = (await this.ctx.storage.get('value')) || 0;
// Increment
value += 1;
// Save back to storage
await this.ctx.storage.put('value', value);
return value;
}
async get(): Promise<number> {
return (await this.ctx.storage.get('value')) || 0;
}
}
// CRITICAL: Export the class
export default Counter;Configure wrangler.jsonc:
jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects binding
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // How you access it: env.COUNTER
"class_name": "Counter" // MUST match exported class name
}
]
},
// REQUIRED: Migration for new DO class
"migrations": [
{
"tag": "v1", // Unique migration identifier
"new_sqlite_classes": [ // Use SQLite backend (recommended)
"Counter"
]
}
]
}Call from Worker ():
src/index.tstypescript
import { Counter } from './counter';
interface Env {
COUNTER: DurableObjectNamespace<Counter>;
}
export { Counter };
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Get Durable Object stub by name
const id = env.COUNTER.idFromName('global-counter');
const stub = env.COUNTER.get(id);
// Call RPC method on the DO
const count = await stub.increment();
return new Response(`Count: ${count}`);
},
};Deploy:
bash
npx wrangler deploybash
cd my-existing-worker
npm install -D @cloudflare/workers-types创建Durable Object类 ():
src/counter.tstypescript
import { DurableObject } from 'cloudflare:workers';
export class Counter extends DurableObject {
async increment(): Promise<number> {
// 从存储中获取当前值(默认0)
let value: number = (await this.ctx.storage.get('value')) || 0;
// 递增
value += 1;
// 保存回存储
await this.ctx.storage.put('value', value);
return value;
}
async get(): Promise<number> {
return (await this.ctx.storage.get('value')) || 0;
}
}
// 关键:导出类
export default Counter;配置wrangler.jsonc:
jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects绑定
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // 代码中访问时使用的名称:env.COUNTER
"class_name": "Counter" // 必须与导出的类名匹配
}
]
},
// 必填:新DO类的迁移配置
"migrations": [
{
"tag": "v1", // 唯一的迁移标识符
"new_sqlite_classes": [ // 使用SQLite后端(推荐)
"Counter"
]
}
]
}从Worker中调用 ():
src/index.tstypescript
import { Counter } from './counter';
interface Env {
COUNTER: DurableObjectNamespace<Counter>;
}
export { Counter };
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 通过名称获取Durable Object存根
const id = env.COUNTER.idFromName('global-counter');
const stub = env.COUNTER.get(id);
// 调用DO上的RPC方法
const count = await stub.increment();
return new Response(`计数:${count}`);
},
};部署:
bash
npx wrangler deployDurable Object Class Structure
Durable Object类结构
Base Class Pattern
基类模式
All Durable Objects MUST extend from :
DurableObjectcloudflare:workerstypescript
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Optional: Initialize from storage
ctx.blockConcurrencyWhile(async () => {
// Load state before handling requests
this.someValue = await ctx.storage.get('someKey') || defaultValue;
});
}
// RPC methods (recommended)
async myMethod(): Promise<string> {
return 'Hello from DO!';
}
// Optional: HTTP fetch handler
async fetch(request: Request): Promise<Response> {
return new Response('Hello from DO fetch!');
}
}
// CRITICAL: Export the class
export default MyDurableObject;所有Durable Objects必须继承中的:
cloudflare:workersDurableObjecttypescript
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// 可选:从存储初始化
ctx.blockConcurrencyWhile(async () => {
// 在处理请求前加载状态
this.someValue = await ctx.storage.get('someKey') || defaultValue;
});
}
// RPC方法(推荐)
async myMethod(): Promise<string> {
return '来自DO的问候!';
}
// 可选:HTTP fetch处理器
async fetch(request: Request): Promise<Response> {
return new Response('来自DO fetch的问候!');
}
}
// 关键:导出类
export default MyDurableObject;Constructor Pattern
构造函数模式
typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // REQUIRED
// Access to environment bindings
this.env = env;
// this.ctx provides:
// - this.ctx.storage (storage API)
// - this.ctx.id (unique ID)
// - this.ctx.waitUntil() (background tasks)
// - this.ctx.acceptWebSocket() (WebSocket hibernation)
}CRITICAL Rules:
- ✅ Always call first
super(ctx, env) - ✅ Keep constructor minimal - heavy work blocks hibernation wake-up
- ✅ Use to initialize from storage before requests
ctx.blockConcurrencyWhile() - ❌ Never use or
setTimeout- breaks hibernation (use alarms instead)setInterval - ❌ Don't rely only on in-memory state with WebSockets - persist to storage
typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // 必填
// 访问环境绑定
this.env = env;
// this.ctx提供以下能力:
// - this.ctx.storage (存储API)
// - this.ctx.id (唯一ID)
// - this.ctx.waitUntil() (后台任务)
// - this.ctx.acceptWebSocket() (WebSocket休眠)
}关键规则:
- ✅ 必须首先调用
super(ctx, env) - ✅ 保持构造函数轻量化 - 繁重的工作会阻止休眠唤醒
- ✅ 使用在处理请求前从存储初始化
ctx.blockConcurrencyWhile() - ❌ 永远不要使用或
setTimeout- 会破坏休眠(改用alarms)setInterval - ❌ 不要仅依赖内存状态处理WebSocket - 要持久化到存储
Exporting the Class
导出类
typescript
// Export as default (required for Worker to use it)
export default MyDurableObject;
// Also export as named export (for type inference in Worker)
export { MyDurableObject };In Worker:
typescript
// Import the class for types
import { MyDurableObject } from './my-durable-object';
// Export it so Worker can instantiate it
export { MyDurableObject };
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
}typescript
// 作为默认导出(Worker使用它的必要条件)
export default MyDurableObject;
// 同时导出命名导出(用于Worker中的类型推断)
export { MyDurableObject };在Worker中:
typescript
// 导入类以获取类型
import { MyDurableObject } from './my-durable-object';
// 导出它以便Worker可以实例化
export { MyDurableObject };
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
}State API - Persistent Storage
状态API - 持久化存储
Durable Objects provide two storage APIs depending on the backend:
- SQL API (SQLite backend) - Recommended
- Key-Value API (KV or SQLite backend)
Durable Objects根据后端提供两种存储API:
- SQL API(SQLite后端)- 推荐
- 键值API(KV或SQLite后端)
Enable SQLite Backend (Recommended)
启用SQLite后端(推荐)
In migrations:
wrangler.jsoncjsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDurableObject"] // ← Use this for SQLite
}
]
}Why SQLite?
- ✅ Up to 1GB storage (vs 128MB for KV backend)
- ✅ Atomic operations (deleteAll is all-or-nothing)
- ✅ SQL queries with transactions
- ✅ Point-in-time recovery (PITR)
- ✅ Synchronous KV API available too
在的迁移配置中:
wrangler.jsoncjsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDurableObject"] // ← 使用此配置启用SQLite
}
]
}为什么选择SQLite?
- ✅ 最大1GB存储(KV后端为128MB)
- ✅ 原子操作(deleteAll是全有或全无)
- ✅ SQL查询支持事务
- ✅ 时间点恢复(PITR)
- ✅ 同时支持同步KV API
SQL API
SQL API
Access via :
ctx.storage.sqltypescript
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Create table on first run
this.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
user TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages(created_at);
`);
}
async addMessage(text: string, user: string): Promise<number> {
// Insert with exec (returns cursor)
const cursor = this.sql.exec(
'INSERT INTO messages (text, user, created_at) VALUES (?, ?, ?) RETURNING id',
text,
user,
Date.now()
);
const row = cursor.one<{ id: number }>();
return row.id;
}
async getMessages(limit: number = 50): Promise<any[]> {
const cursor = this.sql.exec(
'SELECT * FROM messages ORDER BY created_at DESC LIMIT ?',
limit
);
// Convert cursor to array
return cursor.toArray();
}
async deleteOldMessages(beforeTimestamp: number): Promise<void> {
this.sql.exec(
'DELETE FROM messages WHERE created_at < ?',
beforeTimestamp
);
}
}SQL API Methods:
typescript
// Execute query (returns cursor)
const cursor = this.sql.exec('SELECT * FROM table WHERE id = ?', id);
// Get single row
const row = cursor.one<RowType>();
// Get first row or null
const row = cursor.one<RowType>({ allowNone: true });
// Get all rows as array
const rows = cursor.toArray<RowType>();
// Iterate cursor
for (const row of cursor) {
// Process row
}
// Transactions (synchronous)
this.ctx.storage.transactionSync(() => {
this.sql.exec('INSERT INTO table1 ...');
this.sql.exec('UPDATE table2 ...');
// All or nothing
});CRITICAL SQL Rules:
- ✅ Always use parameterized queries with placeholders
? - ✅ Create indexes for frequently queried columns
- ✅ Use transactions for multi-statement operations
- ❌ Don't access the hidden table (used internally for KV API)
__cf_kv - ❌ Don't enable SQLite on existing deployed KV-backed DOs (not supported)
通过访问:
ctx.storage.sqltypescript
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject 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 AUTOINCREMENT,
text TEXT NOT NULL,
user TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_created_at ON messages(created_at);
`);
}
async addMessage(text: string, user: string): Promise<number> {
// 插入数据并返回游标
const cursor = this.sql.exec(
'INSERT INTO messages (text, user, created_at) VALUES (?, ?, ?) RETURNING id',
text,
user,
Date.now()
);
const row = cursor.one<{ id: number }>();
return row.id;
}
async getMessages(limit: number = 50): Promise<any[]> {
const cursor = this.sql.exec(
'SELECT * FROM messages ORDER BY created_at DESC LIMIT ?',
limit
);
// 将游标转换为数组
return cursor.toArray();
}
async deleteOldMessages(beforeTimestamp: number): Promise<void> {
this.sql.exec(
'DELETE FROM messages WHERE created_at < ?',
beforeTimestamp
);
}
}SQL API方法:
typescript
// 执行查询(返回游标)
const cursor = this.sql.exec('SELECT * FROM table WHERE id = ?', id);
// 获取单行
const row = cursor.one<RowType>();
// 获取第一行或null
const row = cursor.one<RowType>({ allowNone: true });
// 获取所有行作为数组
const rows = cursor.toArray<RowType>();
// 遍历游标
for (const row of cursor) {
// 处理行
}
// 事务(同步)
this.ctx.storage.transactionSync(() => {
this.sql.exec('INSERT INTO table1 ...');
this.sql.exec('UPDATE table2 ...');
// 全有或全无
});关键SQL规则:
- ✅ 始终使用参数化查询,用作为占位符
? - ✅ 为频繁查询的列创建索引
- ✅ 多语句操作使用事务
- ❌ 不要访问隐藏的表(KV API内部使用)
__cf_kv - ❌ 不要在已部署的KV后端DO上启用SQLite(不支持)
Key-Value API
键值API
Available on both SQLite and KV backends via :
ctx.storagetypescript
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
async increment(): Promise<number> {
// Get value
let count = await this.ctx.storage.get<number>('count') || 0;
// Increment
count += 1;
// Put value back
await this.ctx.storage.put('count', count);
return count;
}
async batchOperations(): Promise<void> {
// Get multiple keys
const map = await this.ctx.storage.get<number>(['key1', 'key2', 'key3']);
// Put multiple keys
await this.ctx.storage.put({
key1: 'value1',
key2: 'value2',
key3: 'value3',
});
// Delete key
await this.ctx.storage.delete('key1');
// Delete multiple keys
await this.ctx.storage.delete(['key2', 'key3']);
}
async listKeys(): Promise<string[]> {
// List all keys
const map = await this.ctx.storage.list();
return Array.from(map.keys());
// List with prefix
const mapWithPrefix = await this.ctx.storage.list({
prefix: 'user:',
limit: 100,
});
}
async deleteAllStorage(): Promise<void> {
// Delete alarm first (if set)
await this.ctx.storage.deleteAlarm();
// Delete all storage (DO will cease to exist after shutdown)
await this.ctx.storage.deleteAll();
}
}KV API Methods:
typescript
// Get single value
const value = await this.ctx.storage.get<T>('key');
// Get multiple values (returns Map)
const map = await this.ctx.storage.get<T>(['key1', 'key2']);
// Put single value
await this.ctx.storage.put('key', value);
// Put multiple values
await this.ctx.storage.put({ key1: value1, key2: value2 });
// Delete single key
await this.ctx.storage.delete('key');
// Delete multiple keys
await this.ctx.storage.delete(['key1', 'key2']);
// List keys
const map = await this.ctx.storage.list<T>({
prefix: 'user:',
limit: 100,
reverse: false
});
// Delete all (atomic on SQLite, may be partial on KV backend)
await this.ctx.storage.deleteAll();
// Transactions (async)
await this.ctx.storage.transaction(async (txn) => {
await txn.put('key1', value1);
await txn.put('key2', value2);
// All or nothing
});Storage Limits:
- SQLite backend: Up to 1GB storage per DO instance
- KV backend: Up to 128MB storage per DO instance
在SQLite和KV后端都可通过访问:
ctx.storagetypescript
import { DurableObject } from 'cloudflare:workers';
export class MyDurableObject extends DurableObject {
async increment(): Promise<number> {
// 获取值
let count = await this.ctx.storage.get<number>('count') || 0;
// 递增
count += 1;
// 保存回值
await this.ctx.storage.put('count', count);
return count;
}
async batchOperations(): Promise<void> {
// 获取多个键
const map = await this.ctx.storage.get<number>(['key1', 'key2', 'key3']);
// 保存多个键
await this.ctx.storage.put({
key1: 'value1',
key2: 'value2',
key3: 'value3',
});
// 删除键
await this.ctx.storage.delete('key1');
// 删除多个键
await this.ctx.storage.delete(['key2', 'key3']);
}
async listKeys(): Promise<string[]> {
// 列出所有键
const map = await this.ctx.storage.list();
return Array.from(map.keys());
// 列出带前缀的键
const mapWithPrefix = await this.ctx.storage.list({
prefix: 'user:',
limit: 100,
});
}
async deleteAllStorage(): Promise<void> {
// 首先删除警报(如果已设置)
await this.ctx.storage.deleteAlarm();
// 删除所有存储(DO关闭后将不复存在)
await this.ctx.storage.deleteAll();
}
}KV API方法:
typescript
// 获取单个值
const value = await this.ctx.storage.get<T>('key');
// 获取多个值(返回Map)
const map = await this.ctx.storage.get<T>(['key1', 'key2']);
// 保存单个值
await this.ctx.storage.put('key', value);
// 保存多个值
await this.ctx.storage.put({ key1: value1, key2: value2 });
// 删除单个键
await this.ctx.storage.delete('key');
// 删除多个键
await this.ctx.storage.delete(['key1', 'key2']);
// 列出键
const map = await this.ctx.storage.list<T>({
prefix: 'user:',
limit: 100,
reverse: false
});
// 删除所有(SQLite后端是原子操作,KV后端可能部分删除)
await this.ctx.storage.deleteAll();
// 事务(异步)
await this.ctx.storage.transaction(async (txn) => {
await txn.put('key1', value1);
await txn.put('key2', value2);
// 全有或全无
});存储限制:
- SQLite后端: 每个DO实例最大1GB存储
- KV后端: 每个DO实例最大128MB存储
WebSocket Hibernation API
WebSocket休眠API
The WebSocket Hibernation API allows Durable Objects to:
- Handle thousands of WebSocket connections per instance
- Hibernate when idle (no messages, no events) to save costs
- Wake up automatically when messages arrive
- Maintain connections without incurring duration charges during idle periods
Use for: Chat rooms, real-time collaboration, multiplayer games, live updates
WebSocket休眠API允许Durable Objects:
- 每个实例处理数千个WebSocket连接
- 空闲时(无消息、无事件)休眠以节省成本
- 有新消息时自动唤醒
- 空闲期间保持连接,不产生时长费用
适用场景: 聊天室、实时协作、多人游戏、实时更新
How Hibernation Works
休眠工作原理
- Active state - DO is in memory, handling messages
- Idle state - No messages for ~10 seconds, DO can hibernate
- Hibernation - In-memory state cleared, WebSockets stay connected to Cloudflare edge
- Wake up - New message arrives → constructor runs → handler method called
CRITICAL: In-memory state is lost on hibernation. Use to persist per-WebSocket metadata.
serializeAttachment()- 活跃状态 - DO在内存中,处理消息
- 空闲状态 - 约10秒无消息,DO可进入休眠
- 休眠状态 - 内存状态被清除,WebSocket保持与Cloudflare边缘节点的连接
- 唤醒 - 新消息到达 → 构造函数运行 → 调用处理器方法
关键注意事项: 休眠时内存状态会丢失。使用持久化每个WebSocket的元数据。
serializeAttachment()WebSocket Server Pattern
WebSocket服务器模式
typescript
import { DurableObject } from 'cloudflare:workers';
export class ChatRoom extends DurableObject {
sessions: Map<WebSocket, { userId: string; username: string }>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Restore WebSocket connections after hibernation
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
// Deserialize attachment (persisted metadata)
const attachment = ws.deserializeAttachment();
this.sessions.set(ws, attachment);
});
}
async fetch(request: Request): Promise<Response> {
// Expect WebSocket upgrade request
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('Expected websocket', { status: 426 });
}
// Create WebSocket pair
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Get user info from URL or headers
const url = new URL(request.url);
const userId = url.searchParams.get('userId') || 'anonymous';
const username = url.searchParams.get('username') || 'Anonymous';
// Accept WebSocket with hibernation
// CRITICAL: Use ctx.acceptWebSocket(), NOT ws.accept()
this.ctx.acceptWebSocket(server);
// Serialize metadata to persist across hibernation
const metadata = { userId, username };
server.serializeAttachment(metadata);
// Track in-memory (will be restored after hibernation)
this.sessions.set(server, metadata);
// Notify others
this.broadcast(`${username} joined`, server);
// Return client WebSocket to browser
return new Response(null, {
status: 101,
webSocket: client,
});
}
// Called when WebSocket receives a message
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const session = this.sessions.get(ws);
if (typeof message === 'string') {
const data = JSON.parse(message);
if (data.type === 'chat') {
// Broadcast to all connections
this.broadcast(`${session?.username}: ${data.text}`, ws);
}
}
}
// Called when WebSocket closes
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
const session = this.sessions.get(ws);
this.sessions.delete(ws);
// Close the WebSocket
ws.close(code, 'Durable Object closing WebSocket');
// Notify others
if (session) {
this.broadcast(`${session.username} left`);
}
}
// Called on WebSocket errors
async webSocketError(ws: WebSocket, error: any): Promise<void> {
console.error('WebSocket error:', error);
const session = this.sessions.get(ws);
this.sessions.delete(ws);
}
// Helper to broadcast to all connections
broadcast(message: string, except?: WebSocket): void {
this.sessions.forEach((session, ws) => {
if (ws !== except && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'message', text: message }));
}
});
}
}WebSocket Handler Methods:
typescript
// Receive message from client
async webSocketMessage(
ws: WebSocket,
message: string | ArrayBuffer
): Promise<void> {
// Handle message
}
// WebSocket closed by client
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
): Promise<void> {
// Cleanup
}
// WebSocket error occurred
async webSocketError(
ws: WebSocket,
error: any
): Promise<void> {
// Handle error
}Hibernation-Safe Patterns:
typescript
// ✅ CORRECT: Use ctx.acceptWebSocket (enables hibernation)
this.ctx.acceptWebSocket(server);
// ❌ WRONG: Don't use ws.accept() (standard API, no hibernation)
server.accept();
// ✅ CORRECT: Persist metadata across hibernation
server.serializeAttachment({ userId: '123', username: 'Alice' });
// ✅ CORRECT: Restore metadata in constructor
constructor(ctx, env) {
super(ctx, env);
ctx.getWebSockets().forEach((ws) => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
// ❌ WRONG: Don't use setTimeout/setInterval (prevents hibernation)
setTimeout(() => { /* ... */ }, 1000); // ❌ NEVER DO THIS
// ✅ CORRECT: Use alarms for scheduled tasks
await this.ctx.storage.setAlarm(Date.now() + 60000);When Hibernation Does NOT Occur:
- or
setTimeoutcallbacks are pendingsetInterval - In-progress request (awaited I/O)
fetch() - Standard WebSocket API is used (not hibernation API)
- Request/event is still being processed
typescript
import { DurableObject } from 'cloudflare:workers';
export class ChatRoom extends DurableObject {
sessions: Map<WebSocket, { userId: string; username: string }>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// 休眠后恢复WebSocket连接
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
// 反序列化附件(持久化的元数据)
const attachment = ws.deserializeAttachment();
this.sessions.set(ws, attachment);
});
}
async fetch(request: Request): Promise<Response> {
// 预期WebSocket升级请求
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader !== 'websocket') {
return new Response('预期WebSocket请求', { status: 426 });
}
// 创建WebSocket对
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// 从URL或头中获取用户信息
const url = new URL(request.url);
const userId = url.searchParams.get('userId') || 'anonymous';
const username = url.searchParams.get('username') || 'Anonymous';
// 接受WebSocket并启用休眠
// 关键:使用ctx.acceptWebSocket(),而非ws.accept()
this.ctx.acceptWebSocket(server);
// 序列化元数据以跨休眠持久化
const metadata = { userId, username };
server.serializeAttachment(metadata);
// 在内存中跟踪(休眠后会恢复)
this.sessions.set(server, metadata);
// 通知其他用户
this.broadcast(`${username} 加入了聊天室`, server);
// 向浏览器返回客户端WebSocket
return new Response(null, {
status: 101,
webSocket: client,
});
}
// 当WebSocket收到消息时调用
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const session = this.sessions.get(ws);
if (typeof message === 'string') {
const data = JSON.parse(message);
if (data.type === 'chat') {
// 广播到所有连接
this.broadcast(`${session?.username}: ${data.text}`, ws);
}
}
}
// 当WebSocket关闭时调用
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
const session = this.sessions.get(ws);
this.sessions.delete(ws);
// 关闭WebSocket
ws.close(code, 'Durable Object 正在关闭WebSocket');
// 通知其他用户
if (session) {
this.broadcast(`${session.username} 离开了聊天室`);
}
}
// 当WebSocket发生错误时调用
async webSocketError(ws: WebSocket, error: any): Promise<void> {
console.error('WebSocket错误:', error);
const session = this.sessions.get(ws);
this.sessions.delete(ws);
}
// 广播到所有连接的辅助方法
broadcast(message: string, except?: WebSocket): void {
this.sessions.forEach((session, ws) => {
if (ws !== except && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'message', text: message }));
}
});
}
}WebSocket处理器方法:
typescript
// 接收来自客户端的消息
async webSocketMessage(
ws: WebSocket,
message: string | ArrayBuffer
): Promise<void> {
// 处理消息
}
// 客户端关闭WebSocket时调用
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean
): Promise<void> {
// 清理
}
// WebSocket发生错误时调用
async webSocketError(
ws: WebSocket,
error: any
): Promise<void> {
// 处理错误
}休眠安全模式:
typescript
// ✅ 正确:使用ctx.acceptWebSocket(启用休眠)
this.ctx.acceptWebSocket(server);
// ❌ 错误:不要使用ws.accept()(标准API,无休眠)
server.accept();
// ✅ 正确:跨休眠持久化元数据
server.serializeAttachment({ userId: '123', username: 'Alice' });
// ✅ 正确:在构造函数中恢复元数据
constructor(ctx, env) {
super(ctx, env);
ctx.getWebSockets().forEach((ws) => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
// ❌ 错误:不要使用setTimeout/setInterval(阻止休眠)
setTimeout(() => { /* ... */ }, 1000); // ❌ 永远不要这样做
// ✅ 正确:使用alarms处理定时任务
await this.ctx.storage.setAlarm(Date.now() + 60000);以下情况不会触发休眠:
- 有或
setTimeout回调等待执行setInterval - 有正在进行的请求(等待I/O)
fetch() - 使用了标准WebSocket API(而非休眠API)
- 请求/事件仍在处理中
Alarms API - Scheduled Tasks
Alarms API - 定时任务
The Alarms API allows Durable Objects to schedule themselves to wake up at a specific time in the future.
Use for: Batching, cleanup jobs, reminders, periodic tasks, delayed operations
Alarms API允许Durable Objects在未来特定时间调度自己唤醒。
适用场景: 批量处理、清理任务、提醒、周期性任务、延迟操作
Basic Alarm Pattern
基础Alarm模式
typescript
import { DurableObject } from 'cloudflare:workers';
export class Batcher extends DurableObject {
buffer: string[];
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// Restore buffer from storage
this.buffer = await ctx.storage.get('buffer') || [];
});
}
async addItem(item: string): Promise<void> {
this.buffer.push(item);
await this.ctx.storage.put('buffer', this.buffer);
// Schedule alarm for 10 seconds from now (if not already set)
const currentAlarm = await this.ctx.storage.getAlarm();
if (currentAlarm === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000);
}
}
// Called when alarm fires
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
console.log(`Alarm fired (retry count: ${alarmInfo.retryCount})`);
// Process batch
if (this.buffer.length > 0) {
await this.processBatch(this.buffer);
// Clear buffer
this.buffer = [];
await this.ctx.storage.put('buffer', []);
}
// Alarm is automatically deleted after successful execution
}
async processBatch(items: string[]): Promise<void> {
// Send to external API, write to database, etc.
console.log(`Processing ${items.length} items:`, items);
}
}Alarm API Methods:
typescript
// Set alarm to fire at specific timestamp
await this.ctx.storage.setAlarm(Date.now() + 60000); // 60 seconds from now
// Set alarm to fire at specific date
await this.ctx.storage.setAlarm(new Date('2025-12-31T23:59:59Z'));
// Get current alarm (null if not set)
const alarmTime = await this.ctx.storage.getAlarm();
// Delete alarm
await this.ctx.storage.deleteAlarm();
// Alarm handler (called when alarm fires)
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
// Do work
}Alarm Behavior:
- ✅ Guaranteed at-least-once execution - will retry on failure
- ✅ Automatic retries - up to 6 retries with exponential backoff (starting at 2 seconds)
- ✅ Persistent - survives DO hibernation and eviction
- ✅ Automatically deleted after successful execution
- ⚠️ One alarm per DO - setting a new alarm overwrites the previous one
Retry Pattern (Idempotent Operations):
typescript
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
if (alarmInfo.retryCount > 3) {
console.error('Alarm failed after 3 retries, giving up');
return;
}
try {
// Idempotent operation (safe to retry)
await this.sendNotification();
} catch (error) {
console.error('Alarm failed:', error);
throw error; // Will trigger retry
}
}typescript
import { DurableObject } from 'cloudflare:workers';
export class Batcher extends DurableObject {
buffer: string[];
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// 从存储恢复缓冲区
this.buffer = await ctx.storage.get('buffer') || [];
});
}
async addItem(item: string): Promise<void> {
this.buffer.push(item);
await this.ctx.storage.put('buffer', this.buffer);
// 10秒后调度警报(如果尚未设置)
const currentAlarm = await this.ctx.storage.getAlarm();
if (currentAlarm === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000);
}
}
// 警报触发时调用
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
console.log(`警报触发(重试次数:${alarmInfo.retryCount})`);
// 处理批处理
if (this.buffer.length > 0) {
await this.processBatch(this.buffer);
// 清空缓冲区
this.buffer = [];
await this.ctx.storage.put('buffer', []);
}
// 执行成功后警报会自动删除
}
async processBatch(items: string[]): Promise<void> {
// 发送到外部API、写入数据库等
console.log(`处理 ${items.length} 个条目:`, items);
}
}Alarm API方法:
typescript
// 设置警报在特定时间戳触发
await this.ctx.storage.setAlarm(Date.now() + 60000); // 60秒后
// 设置警报在特定日期触发
await this.ctx.storage.setAlarm(new Date('2025-12-31T23:59:59Z'));
// 获取当前警报(未设置则返回null)
const alarmTime = await this.ctx.storage.getAlarm();
// 删除警报
await this.ctx.storage.deleteAlarm();
// 警报处理器(警报触发时调用)
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
// 执行任务
}Alarm行为:
- ✅ 至少执行一次保证 - 失败时会重试
- ✅ 自动重试 - 最多6次重试,使用指数退避(从2秒开始)
- ✅ 持久化 - 即使DO休眠或被驱逐也能保留
- ✅ 执行成功后自动删除
- ⚠️ 每个DO只能有一个警报 - 设置新警报会覆盖旧警报
重试模式(幂等操作):
typescript
async alarm(alarmInfo: { retryCount: number; isRetry: boolean }): Promise<void> {
if (alarmInfo.retryCount > 3) {
console.error('3次重试后失败,放弃执行');
return;
}
try {
// 幂等操作(重试安全)
await this.sendNotification();
} catch (error) {
console.error('警报执行失败:', error);
throw error; // 触发重试
}
}RPC vs HTTP Fetch
RPC vs HTTP Fetch
Durable Objects support two invocation patterns:
- RPC (Remote Procedure Call) - Recommended for new projects
- HTTP Fetch - For HTTP request/response flows or legacy compatibility
Durable Objects支持两种调用模式:
- RPC(远程过程调用) - 推荐用于新项目
- HTTP Fetch - 用于HTTP请求/响应流或遗留兼容性
RPC Pattern (Recommended)
RPC模式(推荐)
Enable RPC with compatibility date :
>= 2024-04-03jsonc
{
"compatibility_date": "2025-10-22"
}Define RPC methods on DO class:
typescript
export class Counter extends DurableObject {
// Public RPC methods (automatically exposed)
async increment(): Promise<number> {
let value = await this.ctx.storage.get<number>('count') || 0;
value += 1;
await this.ctx.storage.put('count', value);
return value;
}
async decrement(): Promise<number> {
let value = await this.ctx.storage.get<number>('count') || 0;
value -= 1;
await this.ctx.storage.put('count', value);
return value;
}
async get(): Promise<number> {
return await this.ctx.storage.get<number>('count') || 0;
}
}Call from Worker:
typescript
// Get stub
const id = env.COUNTER.idFromName('my-counter');
const stub = env.COUNTER.get(id);
// Call RPC methods directly
const count = await stub.increment();
const current = await stub.get();RPC Benefits:
- ✅ Type-safe - TypeScript knows method signatures
- ✅ Simple - Direct method calls, no HTTP ceremony
- ✅ Automatic serialization - Handles structured data
- ✅ Exception propagation - Errors thrown in DO are received in Worker
启用RPC需要兼容性日期:
>= 2024-04-03jsonc
{
"compatibility_date": "2025-10-22"
}在DO类上定义RPC方法:
typescript
export class Counter extends DurableObject {
// 公共RPC方法(自动暴露)
async increment(): Promise<number> {
let value = await this.ctx.storage.get<number>('count') || 0;
value += 1;
await this.ctx.storage.put('count', value);
return value;
}
async decrement(): Promise<number> {
let value = await this.ctx.storage.get<number>('count') || 0;
value -= 1;
await this.ctx.storage.put('count', value);
return value;
}
async get(): Promise<number> {
return await this.ctx.storage.get<number>('count') || 0;
}
}从Worker中调用:
typescript
// 获取存根
const id = env.COUNTER.idFromName('my-counter');
const stub = env.COUNTER.get(id);
// 直接调用RPC方法
const count = await stub.increment();
const current = await stub.get();RPC优势:
- ✅ 类型安全 - TypeScript知晓方法签名
- ✅ 简单易用 - 直接方法调用,无需HTTP繁琐流程
- ✅ 自动序列化 - 处理结构化数据
- ✅ 异常传播 - DO中抛出的错误会传递到Worker
HTTP Fetch Pattern
HTTP Fetch模式
Define handler on DO class:
fetch()typescript
export class Counter extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment' && request.method === 'POST') {
let value = await this.ctx.storage.get<number>('count') || 0;
value += 1;
await this.ctx.storage.put('count', value);
return new Response(JSON.stringify({ count: value }));
}
if (url.pathname === '/get' && request.method === 'GET') {
let value = await this.ctx.storage.get<number>('count') || 0;
return new Response(JSON.stringify({ count: value }));
}
return new Response('Not found', { status: 404 });
}
}Call from Worker:
typescript
// Get stub
const id = env.COUNTER.idFromName('my-counter');
const stub = env.COUNTER.get(id);
// Call fetch
const response = await stub.fetch('https://fake-host/increment', {
method: 'POST',
});
const data = await response.json();在DO类上定义处理器:
fetch()typescript
export class Counter extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment' && request.method === 'POST') {
let value = await this.ctx.storage.get<number>('count') || 0;
value += 1;
await this.ctx.storage.put('count', value);
return new Response(JSON.stringify({ count: value }));
}
if (url.pathname === '/get' && request.method === 'GET') {
let value = await this.ctx.storage.get<number>('count') || 0;
return new Response(JSON.stringify({ count: value }));
}
return new Response('未找到', { status: 404 });
}
}从Worker中调用:
typescript
// 获取存根
const id = env.COUNTER.idFromName('my-counter');
const stub = env.COUNTER.get(id);
// 调用fetch
const response = await stub.fetch('https://fake-host/increment', {
method: 'POST',
});
const data = await response.json();When to Use Each
何时使用哪种模式
| Use Case | Recommendation |
|---|---|
| New project | ✅ RPC (simpler, type-safe) |
| HTTP request/response flow | HTTP Fetch |
| Complex routing logic | HTTP Fetch |
| Type safety important | ✅ RPC |
| Legacy compatibility | HTTP Fetch |
| WebSocket upgrades | HTTP Fetch (required) |
| 适用场景 | 推荐方案 |
|---|---|
| 新项目 | ✅ RPC(更简单、类型安全) |
| HTTP请求/响应流 | HTTP Fetch |
| 复杂路由逻辑 | HTTP Fetch |
| 类型安全重要 | ✅ RPC |
| 遗留兼容性 | HTTP Fetch |
| WebSocket升级 | HTTP Fetch(必需) |
Creating Durable Object Stubs and Routing
创建Durable Object存根与路由
To interact with a Durable Object from a Worker, you need to:
- Get a Durable Object ID
- Create a stub from the ID
- Call methods on the stub
要从Worker与Durable Object交互,需要:
- 获取Durable Object ID
- 从ID创建存根
- 调用存根上的方法
Getting Durable Object IDs
获取Durable Object ID
Three methods to create IDs:
三种创建ID的方法:
1. idFromName(name)
- Named DOs (Most Common)
idFromName(name)1. idFromName(name)
- 命名DO(最常用)
idFromName(name)Use when you want consistent routing to the same DO instance based on a name:
typescript
// Same name always routes to same DO instance globally
const roomId = env.CHAT_ROOM.idFromName('room-123');
const userId = env.USER_SESSION.idFromName('user-alice');
const globalCounter = env.COUNTER.idFromName('global');Use for:
- Chat rooms (name = room ID)
- User sessions (name = user ID)
- Per-tenant logic (name = tenant ID)
- Global singletons (name = 'global')
Characteristics:
- ✅ Deterministic - same name = same DO instance
- ✅ Easy to reference - just need the name string
- ⚠️ First access latency - ~100-300ms for global uniqueness check
- ⚠️ Cached after first use - subsequent access is fast
当你希望基于名称一致路由到同一个DO实例时使用:
typescript
// 相同名称始终路由到同一个全局DO实例
const roomId = env.CHAT_ROOM.idFromName('room-123');
const userId = env.USER_SESSION.idFromName('user-alice');
const globalCounter = env.COUNTER.idFromName('global');适用场景:
- 聊天室(名称=房间ID)
- 用户会话(名称=用户ID)
- 每个租户的专属逻辑(名称=租户ID)
- 全局单例(名称='global')
特性:
- ✅ 确定性 - 相同名称=相同DO实例
- ✅ 易于引用 - 只需名称字符串
- ⚠️ 首次访问延迟 - 全局唯一性检查约100-300ms
- ⚠️ 首次访问后缓存 - 后续访问速度快
2. newUniqueId()
- Random IDs
newUniqueId()2. newUniqueId()
- 随机ID
newUniqueId()Use when you need a new, unique DO instance:
typescript
// Creates a random, globally unique ID
const id = env.MY_DO.newUniqueId();
// With jurisdiction restriction (EU data residency)
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// Store the ID for future use
const idString = id.toString();
await env.KV.put('session:123', idString);Use for:
- Creating new sessions/rooms that don't exist yet
- One-time use DOs
- When you don't have a natural name
Characteristics:
- ✅ Lower latency on first use (no global uniqueness check)
- ⚠️ Must store ID to access same DO later
- ⚠️ ID format is opaque - can't derive meaning from it
当你需要新的、唯一的DO实例时使用:
typescript
// 创建随机的全局唯一ID
const id = env.MY_DO.newUniqueId();
// 带辖区限制(欧盟数据驻留)
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// 存储ID以便后续使用
const idString = id.toString();
await env.KV.put('session:123', idString);适用场景:
- 创建新的会话/房间(之前不存在)
- 一次性使用的DO
- 没有自然名称的场景
特性:
- ✅ 首次访问延迟更低(无全局唯一性检查)
- ⚠️ 必须存储ID才能后续访问同一个DO
- ⚠️ ID格式不透明 - 无法从ID推导含义
3. idFromString(idString)
- Recreate from Saved ID
idFromString(idString)3. idFromString(idString)
- 从保存的ID重建
idFromString(idString)Use when you've previously stored an ID and need to recreate it:
typescript
// Get stored ID string (from KV, D1, cookie, etc.)
const idString = await env.KV.get('session:123');
// Recreate ID
const id = env.MY_DO.idFromString(idString);
// Get stub
const stub = env.MY_DO.get(id);Throws exception if:
- ID string is invalid
- ID was not created from the same
DurableObjectNamespace
当你之前存储过ID,需要重建时使用:
typescript
// 获取存储的ID字符串(来自KV、D1、Cookie等)
const idString = await env.KV.get('session:123');
// 重建ID
const id = env.MY_DO.idFromString(idString);
// 获取存根
const stub = env.MY_DO.get(id);以下情况会抛出异常:
- ID字符串无效
- ID不是从同一个创建的
DurableObjectNamespace
Getting Stubs
获取存根
Method 1: get(id)
- From ID
get(id)方法1: get(id)
- 从ID获取
get(id)typescript
const id = env.MY_DO.idFromName('my-instance');
const stub = env.MY_DO.get(id);
// Call methods
await stub.myMethod();typescript
const id = env.MY_DO.idFromName('my-instance');
const stub = env.MY_DO.get(id);
// 调用方法
await stub.myMethod();Method 2: getByName(name)
- Shortcut for Named DOs
getByName(name)方法2: getByName(name)
- 命名DO的快捷方式
getByName(name)typescript
// Shortcut that combines idFromName + get
const stub = env.MY_DO.getByName('my-instance');
// Equivalent to:
// const id = env.MY_DO.idFromName('my-instance');
// const stub = env.MY_DO.get(id);
await stub.myMethod();Recommended for named DOs (cleaner code).
typescript
// 组合idFromName + get的快捷方式
const stub = env.MY_DO.getByName('my-instance');
// 等同于:
// const id = env.MY_DO.idFromName('my-instance');
// const stub = env.MY_DO.get(id);
await stub.myMethod();推荐用于命名DO(代码更简洁)。
Location Hints (Geographic Routing)
位置提示(地理路由)
Control WHERE a Durable Object is created with location hints:
typescript
// Create DO near specific location
const id = env.MY_DO.idFromName('user-alice');
const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // Eastern North America
// Available location hints:
// - 'wnam' - Western North America
// - 'enam' - Eastern North America
// - 'sam' - South America
// - 'weur' - Western Europe
// - 'eeur' - Eastern Europe
// - 'apac' - Asia-Pacific
// - 'oc' - Oceania
// - 'afr' - Africa
// - 'me' - Middle EastWhen to use:
- ✅ Create DO near user's location (lower latency)
- ✅ Data residency requirements (e.g., EU users → weur/eeur)
Limitations:
- ⚠️ Hints are best-effort - not guaranteed
- ⚠️ Only affects first creation - subsequent access uses existing location
- ⚠️ Cannot move existing DOs - once created, location is fixed
使用位置提示控制DO的创建位置:
typescript
// 在特定位置附近创建DO
const id = env.MY_DO.idFromName('user-alice');
const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // 北美东部
// 可用的位置提示:
// - 'wnam' - 北美西部
// - 'enam' - 北美东部
// - 'sam' - 南美
// - 'weur' - 欧洲西部
// - 'eeur' - 欧洲东部
// - 'apac' - 亚太地区
// - 'oc' - 大洋洲
// - 'afr' - 非洲
// - 'me' - 中东适用场景:
- ✅ 在用户位置附近创建DO(降低延迟)
- ✅ 数据驻留要求(如欧盟用户 → weur/eeur)
限制:
- ⚠️ 提示是尽力而为 - 不保证一定会生效
- ⚠️ 仅影响首次创建 - 后续访问使用已有的位置
- ⚠️ 无法移动现有DO - 创建后位置固定
Jurisdiction Restriction (Data Residency)
辖区限制(数据驻留)
Enforce strict data location requirements:
typescript
// Create DO that MUST stay in EU
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// Available jurisdictions:
// - 'eu' - European Union
// - 'fedramp' - FedRAMP (US government)Use for:
- Regulatory compliance (GDPR, FedRAMP)
- Data sovereignty requirements
CRITICAL:
- ✅ Strictly enforced - DO will never leave jurisdiction
- ⚠️ Cannot combine jurisdiction with location hints
- ⚠️ Higher latency for users outside jurisdiction
强制严格的数据位置要求:
typescript
// 创建必须留在欧盟的DO
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
// 可用的辖区:
// - 'eu' - 欧盟
// - 'fedramp' - FedRAMP(美国政府)适用场景:
- 合规要求(GDPR、FedRAMP)
- 数据主权要求
关键注意事项:
- ✅ 严格执行 - DO永远不会离开指定辖区
- ⚠️ 不能组合辖区和位置提示
- ⚠️ 辖区外用户延迟更高
Migrations - Managing DO Classes
迁移 - 管理DO类
Migrations are REQUIRED when you:
- Create a new DO class
- Rename a DO class
- Delete a DO class
- Transfer a DO class to another Worker
Migration Types:
以下情况必须使用迁移:
- 创建新的DO类
- 重命名DO类
- 删除DO类
- 将DO类转移到另一个Worker
迁移类型:
1. Create New DO Class
1. 创建新DO类
jsonc
{
"durable_objects": {
"bindings": [
{
"name": "COUNTER",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1", // Unique identifier for this migration
"new_sqlite_classes": [ // SQLite backend (recommended)
"Counter"
]
}
]
}For KV backend (legacy):
jsonc
{
"migrations": [
{
"tag": "v1",
"new_classes": ["Counter"] // KV backend (128MB limit)
}
]
}CRITICAL:
- ✅ Use for new DOs (up to 1GB storage)
new_sqlite_classes - ❌ Cannot enable SQLite on existing deployed KV-backed DOs
jsonc
{
"durable_objects": {
"bindings": [
{
"name": "COUNTER",
"class_name": "Counter"
}
]
},
"migrations": [
{
"tag": "v1", // 此迁移的唯一标识符
"new_sqlite_classes": [ // SQLite后端(推荐)
"Counter"
]
}
]
}对于KV后端(遗留):
jsonc
{
"migrations": [
{
"tag": "v1",
"new_classes": ["Counter"] // KV后端(128MB限制)
}
]
}关键注意事项:
- ✅ 新DO使用(最大1GB存储)
new_sqlite_classes - ❌ 无法在已部署的KV后端DO上启用SQLite
2. Rename DO Class
2. 重命名DO类
jsonc
{
"durable_objects": {
"bindings": [
{
"name": "MY_DO",
"class_name": "NewClassName" // New class name
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["OldClassName"]
},
{
"tag": "v2", // New migration tag
"renamed_classes": [
{
"from": "OldClassName",
"to": "NewClassName"
}
]
}
]
}What happens:
- ✅ Existing DO instances keep their data
- ✅ Old bindings automatically forward to new class
- ⚠️ Must export new class in Worker code
jsonc
{
"durable_objects": {
"bindings": [
{
"name": "MY_DO",
"class_name": "NewClassName" // 新类名
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["OldClassName"]
},
{
"tag": "v2", // 新迁移标签
"renamed_classes": [
{
"from": "OldClassName",
"to": "NewClassName"
}
]
}
]
}迁移效果:
- ✅ 现有DO实例保留数据
- ✅ 旧绑定自动转发到新类
- ⚠️ 必须导出新类到Worker代码中
3. Delete DO Class
3. 删除DO类
jsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter"]
},
{
"tag": "v2",
"deleted_classes": ["Counter"] // Mark as deleted
}
]
}What happens:
- ✅ Existing DO instances are deleted immediately
- ✅ All storage is deleted
- ⚠️ Cannot undo - data is permanently lost
Before deleting:
- Export data if needed
- Update Workers that reference this DO
jsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter"]
},
{
"tag": "v2",
"deleted_classes": ["Counter"] // 标记为已删除
}
]
}迁移效果:
- ✅ 现有DO实例立即删除
- ✅ 所有存储被删除
- ⚠️ 无法撤销 - 数据永久丢失
删除前注意:
- 如有需要,导出数据
- 更新引用此DO的Worker
4. Transfer DO Class to Another Worker
4. 将DO类转移到另一个Worker
jsonc
// In destination Worker:
{
"durable_objects": {
"bindings": [
{
"name": "TRANSFERRED_DO",
"class_name": "TransferredClass"
}
]
},
"migrations": [
{
"tag": "v1",
"transferred_classes": [
{
"from": "OriginalClass",
"from_script": "original-worker", // Source Worker name
"to": "TransferredClass"
}
]
}
]
}What happens:
- ✅ DO instances move to new Worker
- ✅ All storage is transferred
- ✅ Old bindings automatically forward
- ⚠️ Destination class must be exported
jsonc
// 在目标Worker中:
{
"durable_objects": {
"bindings": [
{
"name": "TRANSFERRED_DO",
"class_name": "TransferredClass"
}
]
},
"migrations": [
{
"tag": "v1",
"transferred_classes": [
{
"from": "OriginalClass",
"from_script": "original-worker", // 源Worker名称
"to": "TransferredClass"
}
]
}
]
}迁移效果:
- ✅ DO实例移动到新Worker
- ✅ 所有存储被转移
- ✅ 旧绑定自动转发
- ⚠️ 必须导出目标类
Migration Rules
迁移规则
CRITICAL Migration Gotchas:
❌ Migrations are ATOMIC - cannot gradual deploy
- All instances migrate at once when you deploy
- No partial rollout support
❌ Migration tags must be unique
- Cannot reuse tags
- Tags are append-only
❌ Cannot enable SQLite on existing KV-backed DOs
- Must create new DO class instead
✅ Code changes don't need migrations
- Only schema changes (new/rename/delete/transfer) need migrations
- You can deploy code updates freely
✅ Global uniqueness is per account
- DO class names are unique across your entire account
- Even across different Workers
关键迁移注意事项:
❌ 迁移是原子操作 - 无法逐步部署
- 部署时所有实例同时迁移
- 不支持部分发布
❌ 迁移标签必须唯一
- 不能重复使用标签
- 标签只能追加
❌ 无法在现有KV后端DO上启用SQLite
- 必须创建新的DO类
✅ 代码变更不需要迁移
- 只有架构变更(新建/重命名/删除/转移)需要迁移
- 可以自由部署代码更新
✅ 全局唯一性按账户划分
- DO类名在整个账户中唯一
- 即使在不同Worker中也是如此
Common Patterns
常见模式
Pattern 1: Rate Limiting (Per-User)
模式1:速率限制(每个用户)
typescript
export class RateLimiter extends DurableObject {
async checkLimit(userId: string, limit: number, window: number): Promise<boolean> {
const key = `rate:${userId}`;
const now = Date.now();
// Get recent requests
const requests = await this.ctx.storage.get<number[]>(key) || [];
// Remove requests outside window
const validRequests = requests.filter(timestamp => now - timestamp < window);
// Check limit
if (validRequests.length >= limit) {
return false; // Rate limit exceeded
}
// Add current request
validRequests.push(now);
await this.ctx.storage.put(key, validRequests);
return true; // Within limit
}
}
// Worker usage:
const limiter = env.RATE_LIMITER.getByName(userId);
const allowed = await limiter.checkLimit(userId, 100, 60000); // 100 req/min
if (!allowed) {
return new Response('Rate limit exceeded', { status: 429 });
}typescript
export class RateLimiter extends DurableObject {
async checkLimit(userId: string, limit: number, window: number): Promise<boolean> {
const key = `rate:${userId}`;
const now = Date.now();
// 获取最近的请求
const requests = await this.ctx.storage.get<number[]>(key) || [];
// 移除窗口外的请求
const validRequests = requests.filter(timestamp => now - timestamp < window);
// 检查限制
if (validRequests.length >= limit) {
return false; // 超出速率限制
}
// 添加当前请求
validRequests.push(now);
await this.ctx.storage.put(key, validRequests);
return true; // 在限制内
}
}
// Worker使用:
const limiter = env.RATE_LIMITER.getByName(userId);
const allowed = await limiter.checkLimit(userId, 100, 60000); // 每分钟100次请求
if (!allowed) {
return new Response('超出速率限制', { status: 429 });
}Pattern 2: Session Management
模式2:会话管理
typescript
export class UserSession 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 session (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER
);
`);
// Schedule cleanup alarm
ctx.blockConcurrencyWhile(async () => {
const alarm = await ctx.storage.getAlarm();
if (alarm === null) {
await ctx.storage.setAlarm(Date.now() + 3600000); // 1 hour
}
});
}
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 get(key: string): Promise<any | null> {
const cursor = this.sql.exec(
'SELECT value, expires_at FROM session WHERE key = ?',
key
);
const row = cursor.one<{ value: string; expires_at: number | null }>({ allowNone: true });
if (!row) {
return null;
}
// Check expiration
if (row.expires_at && row.expires_at < Date.now()) {
this.sql.exec('DELETE FROM session WHERE key = ?', key);
return null;
}
return JSON.parse(row.value);
}
async alarm(): Promise<void> {
// Cleanup expired sessions
this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now());
// Schedule next cleanup
await this.ctx.storage.setAlarm(Date.now() + 3600000);
}
}typescript
export class UserSession 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 session (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER
);
`);
// 调度清理警报
ctx.blockConcurrencyWhile(async () => {
const alarm = await ctx.storage.getAlarm();
if (alarm === null) {
await ctx.storage.setAlarm(Date.now() + 3600000); // 1小时
}
});
}
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 get(key: string): Promise<any | null> {
const cursor = this.sql.exec(
'SELECT value, expires_at FROM session WHERE key = ?',
key
);
const row = cursor.one<{ value: string; expires_at: number | null }>({ allowNone: true });
if (!row) {
return null;
}
// 检查过期
if (row.expires_at && row.expires_at < Date.now()) {
this.sql.exec('DELETE FROM session WHERE key = ?', key);
return null;
}
return JSON.parse(row.value);
}
async alarm(): Promise<void> {
// 清理过期会话
this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now());
// 调度下一次清理
await this.ctx.storage.setAlarm(Date.now() + 3600000);
}
}Pattern 3: Leader Election
模式3:领导者选举
typescript
export class LeaderElection 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 leader (
id INTEGER PRIMARY KEY CHECK (id = 1),
worker_id TEXT NOT NULL,
elected_at INTEGER NOT NULL
);
`);
}
async electLeader(workerId: string): Promise<boolean> {
// Try to become leader
try {
this.sql.exec(
'INSERT INTO leader (id, worker_id, elected_at) VALUES (1, ?, ?)',
workerId,
Date.now()
);
return true; // Became leader
} catch (error) {
return false; // Someone else is leader
}
}
async getLeader(): Promise<string | null> {
const cursor = this.sql.exec('SELECT worker_id FROM leader WHERE id = 1');
const row = cursor.one<{ worker_id: string }>({ allowNone: true });
return row?.worker_id || null;
}
async releaseLeadership(workerId: string): Promise<void> {
this.sql.exec('DELETE FROM leader WHERE id = 1 AND worker_id = ?', workerId);
}
}typescript
export class LeaderElection 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 leader (
id INTEGER PRIMARY KEY CHECK (id = 1),
worker_id TEXT NOT NULL,
elected_at INTEGER NOT NULL
);
`);
}
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 (error) {
return false; // 已有领导者
}
}
async getLeader(): Promise<string | null> {
const cursor = this.sql.exec('SELECT worker_id FROM leader WHERE id = 1');
const row = cursor.one<{ worker_id: string }>({ allowNone: true });
return row?.worker_id || null;
}
async releaseLeadership(workerId: string): Promise<void> {
this.sql.exec('DELETE FROM leader WHERE id = 1 AND worker_id = ?', workerId);
}
}Pattern 4: Multi-DO Coordination
模式4:多DO协调
typescript
// Coordinator DO
export class GameCoordinator extends DurableObject {
async createGame(gameId: string, env: Env): Promise<void> {
// Create game room DO
const gameRoom = env.GAME_ROOM.getByName(gameId);
await gameRoom.initialize();
// Track in coordinator
await this.ctx.storage.put(`game:${gameId}`, {
id: gameId,
created: Date.now(),
});
}
async listGames(): Promise<string[]> {
const games = await this.ctx.storage.list({ prefix: 'game:' });
return Array.from(games.keys()).map(key => key.replace('game:', ''));
}
}
// Game room DO
export class GameRoom extends DurableObject {
async initialize(): Promise<void> {
await this.ctx.storage.put('state', {
players: [],
started: false,
});
}
async addPlayer(playerId: string): Promise<void> {
const state = await this.ctx.storage.get('state');
state.players.push(playerId);
await this.ctx.storage.put('state', state);
}
}typescript
// 协调器DO
export class GameCoordinator extends DurableObject {
async createGame(gameId: string, env: Env): Promise<void> {
// 创建游戏房间DO
const gameRoom = env.GAME_ROOM.getByName(gameId);
await gameRoom.initialize();
// 在协调器中跟踪
await this.ctx.storage.put(`game:${gameId}`, {
id: gameId,
created: Date.now(),
});
}
async listGames(): Promise<string[]> {
const games = await this.ctx.storage.list({ prefix: 'game:' });
return Array.from(games.keys()).map(key => key.replace('game:', ''));
}
}
// 游戏房间DO
export class GameRoom extends DurableObject {
async initialize(): Promise<void> {
await this.ctx.storage.put('state', {
players: [],
started: false,
});
}
async addPlayer(playerId: string): Promise<void> {
const state = await this.ctx.storage.get('state');
state.players.push(playerId);
await this.ctx.storage.put('state', state);
}
}Critical Rules
关键规则
Always Do
必须遵守
✅ Export DO class from Worker
typescript
export class MyDO extends DurableObject { }
export default MyDO; // Required✅ Call in constructor
super(ctx, env)typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // Required first line
}✅ Use for new DOs
new_sqlite_classesjsonc
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }✅ Use for hibernation
ctx.acceptWebSocket()typescript
this.ctx.acceptWebSocket(server); // Enables hibernation✅ Persist critical state to storage (not just memory)
typescript
await this.ctx.storage.put('important', value);✅ Use alarms instead of setTimeout/setInterval
typescript
await this.ctx.storage.setAlarm(Date.now() + 60000);✅ Use parameterized SQL queries
typescript
this.sql.exec('SELECT * FROM table WHERE id = ?', id);✅ Minimize constructor work
typescript
constructor(ctx, env) {
super(ctx, env);
// Minimal initialization only
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}✅ 从Worker中导出DO类
typescript
export class MyDO extends DurableObject { }
export default MyDO; // 必填✅ 在构造函数中调用
super(ctx, env)typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // 必须是第一行
}✅ 新DO使用
new_sqlite_classesjsonc
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }✅ 使用启用休眠
ctx.acceptWebSocket()typescript
this.ctx.acceptWebSocket(server); // 启用休眠✅ 将关键状态持久化到存储(不只是内存)
typescript
await this.ctx.storage.put('important', value);✅ 使用alarms替代setTimeout/setInterval
typescript
await this.ctx.storage.setAlarm(Date.now() + 60000);✅ 使用参数化SQL查询
typescript
this.sql.exec('SELECT * FROM table WHERE id = ?', id);✅ 最小化构造函数工作
typescript
constructor(ctx, env) {
super(ctx, env);
// 仅做最小化初始化
ctx.blockConcurrencyWhile(async () => {
// 从存储加载
});
}Never Do
禁止操作
❌ Create DO without migration
jsonc
// Missing migrations array = error❌ Forget to export DO class
typescript
class MyDO extends DurableObject { }
// Missing: export default MyDO;❌ Use or
setTimeoutsetIntervaltypescript
setTimeout(() => {}, 1000); // Prevents hibernation❌ Rely only on in-memory state with WebSockets
typescript
// ❌ WRONG: this.sessions will be lost on hibernation
// ✅ CORRECT: Use serializeAttachment()❌ Deploy migrations gradually
bash
undefined❌ 创建DO时不配置迁移
jsonc
// 缺少migrations数组会报错❌ 忘记导出DO类
typescript
class MyDO extends DurableObject { }
// 缺少:export default MyDO;❌ 使用或
setTimeoutsetIntervaltypescript
setTimeout(() => {}, 1000); // 阻止休眠❌ 仅依赖内存状态处理WebSocket
typescript
// ❌ 错误:休眠时this.sessions会丢失
// ✅ 正确:使用serializeAttachment()❌ 逐步部署迁移
bash
undefinedMigrations are atomic - cannot use gradual rollout
迁移是原子操作 - 不能使用逐步发布
❌ **Enable SQLite on existing KV-backed DO**
```jsonc
// Not supported - must create new DO class instead❌ Use standard WebSocket API expecting hibernation
typescript
ws.accept(); // ❌ No hibernation
this.ctx.acceptWebSocket(ws); // ✅ Hibernation enabled❌ Assume location hints are guaranteed
typescript
// Location hints are best-effort only
❌ **在现有KV后端DO上启用SQLite**
```jsonc
// 不支持 - 必须创建新的DO类❌ 使用标准WebSocket API却期望休眠
typescript
ws.accept(); // ❌ 无休眠
this.ctx.acceptWebSocket(ws); // ✅ 启用休眠❌ 假设位置提示一定会生效
typescript
// 位置提示只是尽力而为,不保证Known Issues Prevention
已知问题预防
This skill prevents 15+ documented issues:
本指南可预防15+种已记录的问题:
Issue #1: Class Not Exported
问题1:类未导出
Error: or
Source: https://developers.cloudflare.com/durable-objects/get-started/
Why It Happens: DO class not exported from Worker
Prevention:
"binding not found""Class X not found"typescript
export class MyDO extends DurableObject { }
export default MyDO; // ← Required错误信息: 或
来源: https://developers.cloudflare.com/durable-objects/get-started/
原因: DO类未从Worker导出
解决方法:
"binding not found""Class X not found"typescript
export class MyDO extends DurableObject { }
export default MyDO; // ← 必填Issue #2: Missing Migration
问题2:缺少迁移
Error: or
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 required""no migration found for class"jsonc
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
]
}错误信息: 或
来源: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
原因: 创建DO类但未添加迁移条目
解决方法: 创建新DO类时始终添加迁移
"migrations required""no migration found for class"jsonc
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
]
}Issue #3: Wrong Migration Type (KV vs SQLite)
问题3:错误的迁移类型(KV vs SQLite)
Error: Schema errors, storage API mismatch
Source: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
Why It Happens: Used instead of
Prevention: Use for SQLite backend (recommended)
new_classesnew_sqlite_classesnew_sqlite_classes错误信息: 架构错误、存储API不匹配
来源: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
原因: 使用了而非
解决方法: 推荐使用启用SQLite后端
new_classesnew_sqlite_classesnew_sqlite_classesIssue #4: Constructor Overhead Blocks Hibernation Wake
问题4:构造函数开销阻止休眠唤醒
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()typescript
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}错误信息: 休眠唤醒时间慢
来源: https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/
原因: 构造函数中执行繁重工作
解决方法: 最小化构造函数,使用
blockConcurrencyWhile()typescript
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// 从存储加载
});
}Issue #5: setTimeout Breaks Hibernation
问题5:setTimeout破坏休眠
Error: DO never hibernates, high duration charges
Source: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
Why It Happens: / prevents hibernation
Prevention: Use alarms API instead
setTimeoutsetIntervaltypescript
// ❌ WRONG
setTimeout(() => {}, 1000);
// ✅ CORRECT
await this.ctx.storage.setAlarm(Date.now() + 1000);错误信息: DO永远不休眠,时长费用高
来源: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
原因: /阻止休眠
解决方法: 使用alarms API替代
setTimeoutsetIntervaltypescript
// ❌ 错误
setTimeout(() => {}, 1000);
// ✅ 正确
await this.ctx.storage.setAlarm(Date.now() + 1000);Issue #6: In-Memory State Lost on Hibernation
问题6:休眠时内存状态丢失
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 for WebSocket metadata
serializeAttachment()typescript
ws.serializeAttachment({ userId, username });
// Restore in constructor
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});错误信息: WebSocket元数据丢失,状态意外重置
来源: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
原因: 依赖休眠时会被清除的内存状态
解决方法: 使用存储WebSocket元数据
serializeAttachment()typescript
ws.serializeAttachment({ userId, username });
// 在构造函数中恢复
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});Issue #7: Outgoing WebSocket Cannot Hibernate
问题7:出站WebSocket无法休眠
Error: High charges despite hibernation API
Source: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
Why It Happens: Outgoing WebSockets don't support hibernation
Prevention: Only use hibernation for server-side (incoming) WebSockets
错误信息: 尽管使用了休眠API,费用仍很高
来源: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
原因: 出站WebSocket不支持休眠
解决方法: 仅对服务器端(入站)WebSocket使用休眠
Issue #8: Global Uniqueness Confusion
问题8:全局唯一性混淆
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
错误信息: 意外的DO类名冲突
来源: https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness
原因: DO类名在整个账户中全局唯一
解决方法: 了解DO类名在账户内所有Worker中共享
Issue #9: Partial deleteAll on KV Backend
问题9:KV后端上的partial deleteAll
Error: Storage not fully deleted, billing continues
Source: https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
Why It Happens: KV backend can fail partially
Prevention: Use SQLite backend for atomic deleteAll
deleteAll()错误信息: 存储未完全删除,仍产生账单
来源: https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
原因: KV后端的可能部分失败
解决方法: 使用SQLite后端实现原子deleteAll
deleteAll()Issue #10: Binding Name Mismatch
问题10:绑定名称不匹配
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
jsonc
{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }typescript
env.MY_DO.getByName('instance'); // Must match binding name错误信息: 运行时访问DO绑定时出错
来源: https://developers.cloudflare.com/durable-objects/get-started/
原因: wrangler.jsonc中的绑定名称与代码不匹配
解决方法: 确保名称一致
jsonc
{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }typescript
env.MY_DO.getByName('instance'); // 必须与绑定名称匹配Issue #11: State Size Exceeded
问题11:超出状态大小限制
Error: 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
"state limit exceeded"错误信息: 或存储错误
来源: https://developers.cloudflare.com/durable-objects/platform/pricing/
原因: 超出了1GB(SQLite)或128MB(KV)的限制
解决方法: 监控存储大小,使用alarms实现清理
"state limit exceeded"Issue #12: Migration Not Atomic
问题12:迁移不是原子操作
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
错误信息: 逐步部署被阻止
来源: https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/
原因: 尝试结合逐步发布和迁移
解决方法: 迁移会在所有实例中原子部署
Issue #13: Location Hint Ignored
问题13:位置提示被忽略
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
错误信息: DO创建在错误的区域
来源: https://developers.cloudflare.com/durable-objects/reference/data-location/
原因: 位置提示是尽力而为,不保证生效
解决方法: 使用辖区限制满足严格要求
Issue #14: Alarm Retry Failures
问题14:警报重试失败
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
typescript
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
return;
}
// Idempotent operation
}错误信息: 警报失败后任务丢失
来源: https://developers.cloudflare.com/durable-objects/api/alarms/
原因: 警报处理器反复抛出错误
解决方法: 实现幂等的警报处理器
typescript
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('3次重试后放弃');
return;
}
// 幂等操作
}Issue #15: Fetch Blocks Hibernation
问题15:Fetch阻止休眠
Error: DO never hibernates despite using hibernation API
Source: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
Why It Happens: In-progress requests prevent hibernation
Prevention: Ensure all async I/O completes before idle period
fetch()错误信息: 尽管使用了休眠API,DO永远不休眠
来源: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
原因: 正在进行的请求阻止休眠
解决方法: 确保空闲前所有异步I/O完成
fetch()Configuration Reference
配置参考
Complete wrangler.jsonc Example
完整的wrangler.jsonc示例
jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects configuration
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // Binding name (use as env.COUNTER)
"class_name": "Counter" // Must match exported class
},
{
"name": "CHAT_ROOM",
"class_name": "ChatRoom"
}
]
},
// Migrations (required for all DO changes)
"migrations": [
{
"tag": "v1", // Initial migration
"new_sqlite_classes": [
"Counter",
"ChatRoom"
]
},
{
"tag": "v2", // Rename example
"renamed_classes": [
{
"from": "Counter",
"to": "CounterV2"
}
]
}
]
}jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-22",
// Durable Objects配置
"durable_objects": {
"bindings": [
{
"name": "COUNTER", // 绑定名称(通过env.COUNTER访问)
"class_name": "Counter" // 必须与导出的类名匹配
},
{
"name": "CHAT_ROOM",
"class_name": "ChatRoom"
}
]
},
// 迁移(所有DO变更都需要)
"migrations": [
{
"tag": "v1", // 初始迁移
"new_sqlite_classes": [
"Counter",
"ChatRoom"
]
},
{
"tag": "v2", // 重命名示例
"renamed_classes": [
{
"from": "Counter",
"to": "CounterV2"
}
]
}
]
}TypeScript Types
TypeScript类型
typescript
import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';
// Environment bindings
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
DB: D1Database;
// ... other bindings
}
// Durable Object class
export class MyDurableObject extends DurableObject<Env> {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
}
async myMethod(): Promise<string> {
// Access env bindings
await this.env.DB.prepare('...').run();
return 'Hello';
}
}typescript
import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';
// 环境绑定
interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
DB: D1Database;
// ... 其他绑定
}
// Durable Object类
export class MyDurableObject extends DurableObject<Env> {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
}
async myMethod(): Promise<string> {
// 访问环境绑定
await this.env.DB.prepare('...').run();
return 'Hello';
}
}Official Documentation
官方文档
- Durable Objects: https://developers.cloudflare.com/durable-objects/
- State API (SQL): https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
- WebSocket Hibernation: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
- Alarms API: https://developers.cloudflare.com/durable-objects/api/alarms/
- Migrations: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
- Best Practices: https://developers.cloudflare.com/durable-objects/best-practices/
- Pricing: https://developers.cloudflare.com/durable-objects/platform/pricing/
Questions? Issues?
- Check for common problems
references/top-errors.md - Review for working examples
templates/ - Consult official docs: https://developers.cloudflare.com/durable-objects/
- Verify migrations configuration carefully
- Durable Objects: https://developers.cloudflare.com/durable-objects/
- 状态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/
- 仔细验证迁移配置