mcpField
C
cloudflare-durable-objects

cloudflare-durable-objects

github.com/jackspace云与 DevOps1641 次安装12 周前更新
代码仓库

关于 cloudflare-durable-objects

cloudflare-durable-objects 是面向 Cloudflare Workers 开发者的 Durable Objects 开发指南,涵盖有状态对象的存储、通信、定时任务和调用方式。内容包含项目搭建、代码模式及 SQLite、WebSocket 休眠、Alarms API 等实践说明。

主要功能

  1. 01搭建 Durable Objects 项目
  2. 02使用 SQLite 与键值存储
  3. 03通过 WebSocket 休眠处理连接
  4. 04使用 Alarms API 安排任务
  5. 05通过 RPC 或 HTTP 调用对象

适用场景

  1. 01构建实时协作或聊天服务
  2. 02实现会话管理与协调逻辑
  3. 03安排清理、提醒等延迟任务

安装

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

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-durable-objects

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

附带文件README.mdSKILL.json

详细介绍

Cloudflare Durable Objects

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


什么是 Durable Objects?

Cloudflare Durable Objects 是全局唯一的有状态对象,提供:

  • 单点协调 - 每个 Durable Object 实例在 Cloudflare 的网络中全局唯一
  • 强一致性 - 支持事务且可串行化的存储(提供 ACID 保证)
  • 实时通信 - WebSocket 休眠 API 支持每个实例处理数千个连接
  • 持久化状态 - 内置 SQLite 数据库(最高 1GB)或键值存储
  • 定时任务 - Alarms API 可用于在未来执行任务
  • 全球分发 - 自动路由到最佳位置
  • 自动扩缩 - 可创建数百万个独立实例

适用场景:

  • 聊天室和实时协作
  • 多人游戏服务
  • 速率限制和会话管理
  • 领导者选举和协调
  • 支持休眠的 WebSocket 服务
  • 有状态工作流和队列
  • 面向每位用户或每个房间的逻辑

快速开始(10 分钟)

选项 1:搭建新的 DO 项目
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
  • Durable Object 类实现示例
  • 用于调用 Durable Object 的 Worker
选项 2:添加到现有 Worker
cd my-existing-worker
npm install -D @cloudflare/workers-types

创建 Durable Object 类(src/counter.ts):

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;

配置 wrangler.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"
      ]
    }
  ]
}

从 Worker 调用(src/index.ts):

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}`);
  },
};

部署:

npx wrangler deploy

Durable Object 类结构

基类模式

所有 Durable Object 都必须扩展 cloudflare:workers 中的 DurableObject:

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;
构造函数模式
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)
}

重要规则:

  • ✅ 始终首先调用 super(ctx, env)
  • ✅ 保持构造函数精简 - 繁重操作会阻塞休眠唤醒
  • ✅ 使用 ctx.blockConcurrencyWhile() 在处理请求前从存储中初始化
  • ❌ 切勿使用 setTimeout 或 setInterval - 这会破坏休眠机制(请改用 alarms)
  • ❌ 使用 WebSocket 时,不要只依赖内存状态 - 请将状态持久化到存储中
导出类
// Export as default (required for Worker to use it)
export default MyDurableObject;

// Also export as named export (for type inference in Worker)
export { MyDurableObject };

在 Worker 中:

// 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>;
}

状态 API - 持久化存储

Durable Objects 根据后端提供两种存储 API:

  1. SQL API(SQLite 后端)- 推荐
  2. 键值 API(KV 或 SQLite 后端)
启用 SQLite 后端(推荐)

在 wrangler.jsonc 的 migrations 中:

{
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["MyDurableObject"]  // ← Use this for SQLite
    }
  ]
}

为什么选择 SQLite?

  • ✅ 存储空间最高可达 1GB(KV 后端为 128MB)
  • ✅ 原子操作(deleteAll 要么全部执行,要么完全不执行)
  • ✅ 支持事务的 SQL 查询
  • ✅ 时间点恢复(PITR)
  • ✅ 也提供同步 KV API
SQL API

通过 ctx.storage.sql 访问:

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 方法:

// 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
});

重要 SQL 规则:

  • ✅ 始终使用带 ? 占位符的参数化查询
  • ✅ 为频繁查询的列创建索引
  • ✅ 多语句操作应使用事务
  • ❌ 不要访问隐藏的 __cf_kv 表(KV API 在内部使用)
  • ❌ 不要在已部署的、由 KV 后端支持的 Durable Objects 上启用 SQLite(不支持)
键值 API

SQLite 和 KV 后端都可通过 ctx.storage 使用:

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 方法:

// 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
});

存储限制:

  • SQLite 后端:每个 Durable Object 实例最多 1GB 存储空间
  • KV 后端:每个 Durable Object 实例最多 128MB 存储空间

WebSocket 休眠 API

WebSocket 休眠 API 允许 Durable Objects:

  • 在每个实例中处理 数千个 WebSocket 连接
  • 空闲时(没有消息、没有事件)进入休眠,以节省成本
  • 在收到消息时自动唤醒
  • 在空闲期间维持连接,且不会产生持续时间费用

适用场景: 聊天室、实时协作、多人游戏、实时更新

休眠如何运作
  1. 活动状态 - DO 位于内存中,正在处理消息
  2. 空闲状态 - 约 10 秒内没有消息,DO 可以进入休眠
  3. 休眠 - 内存中的状态被清除,WebSocket 仍保持与 Cloudflare 边缘节点的连接
  4. 唤醒 - 收到新消息 → 运行构造函数 → 调用处理程序方法

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

WebSocket 服务端模式
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 处理程序方法:

// 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
}

休眠安全模式:

// ✅ 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);

不会进入休眠的情况:

  • 有待执行的 setTimeout 或 setInterval 回调
  • 正在进行的 fetch() 请求(等待中的 I/O)
  • 使用了标准 WebSocket API(而非休眠 API)
  • 请求或事件仍在处理中

Alarms API - 计划任务

Alarms API 允许 Durable Objects 安排自己在未来的指定时间唤醒。

适用场景: 批处理、清理任务、提醒、定期任务、延迟操作

基本 Alarm 模式
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 方法:

// 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 行为:

  • ✅ 保证至少执行一次 - 失败时会重试
  • ✅ 自动重试 - 最多重试 6 次,采用指数退避(从 2 秒开始)
  • ✅ 持久化 - 经受 DO 休眠和逐出后仍然保留
  • ✅ 成功执行后自动删除
  • ⚠️ 每个 DO 只能设置一个 Alarm - 设置新 Alarm 会覆盖上一个

重试模式(幂等操作):

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
  }
}

RPC 与 HTTP Fetch

Durable Objects 支持两种调用模式:

  1. RPC(远程过程调用) - 新项目推荐使用
  2. HTTP Fetch - 用于 HTTP 请求/响应流程或兼容旧版
RPC 模式(推荐)

使用兼容日期 >= 2024-04-03 启用 RPC:

{
  "compatibility_date": "2025-10-22"
}

在 DO 类上定义 RPC 方法:

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;
  }
}

从 Worker 中调用:

// 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 优势:

  • ✅ 类型安全 - TypeScript 知道方法签名
  • ✅ 简单 - 直接调用方法,无需 HTTP 流程
  • ✅ 自动序列化 - 处理结构化数据
  • ✅ 异常传播 - DO 中抛出的错误会传递到 Worker
HTTP Fetch 模式

在 DO 类上定义 fetch() 处理程序:

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 });
  }
}

从 Worker 中调用:

// 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();
如何选择
用例建议
新项目✅ RPC(更简单、类型安全)
HTTP 请求/响应流程HTTP Fetch
复杂路由逻辑HTTP Fetch
类型安全很重要✅ RPC
兼容旧版HTTP Fetch
WebSocket 升级HTTP Fetch(必需)

创建 Durable Object 存根并进行路由

要从 Worker 与 Durable Object 交互,你需要:

  1. 获取一个 Durable Object ID
  2. 根据该 ID 创建一个存根
  3. 调用存根上的方法
获取 Durable Object ID

创建 ID 的三种方法:

1. idFromName(name) - 命名 DO(最常见)

当你希望根据名称将请求一致地路由到同一个 DO 实例时使用:

// 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');

适用于:

  • 聊天室(名称 = 房间 ID)
  • 用户会话(名称 = 用户 ID)
  • 按租户划分的逻辑(名称 = 租户 ID)
  • 全局单例(名称 = 'global')

特性:

  • ✅ 确定性 - 相同名称 = 相同 DO 实例
  • ✅ 易于引用 - 只需要名称字符串
  • ⚠️ 首次访问延迟 - 检查全局唯一性约需 100-300 毫秒
  • ⚠️ 首次使用后会缓存 - 后续访问速度很快
2. newUniqueId() - 随机 ID

需要新的唯一 DO 实例时使用:

// 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);

适用场景:

  • 创建尚不存在的新会话/房间
  • 一次性使用的 DO
  • 没有自然名称时

特点:

  • ✅ 首次使用时延迟较低(无需检查全局唯一性)
  • ⚠️ 必须保存 ID,才能在之后访问同一个 DO
  • ⚠️ ID 格式不透明,无法从中推断含义
3. idFromString(idString) - 根据已保存的 ID 重新创建

如果你之前保存过 ID,需要重新创建时使用:

// 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);

以下情况会抛出异常:

  • ID 字符串无效
  • ID 并非由同一个 DurableObjectNamespace 创建
获取存根
方法 1:get(id) - 根据 ID 获取
const id = env.MY_DO.idFromName('my-instance');
const stub = env.MY_DO.get(id);

// Call methods
await stub.myMethod();
方法 2:getByName(name) - 命名 DO 的快捷方式
// 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();

命名 DO 推荐使用此方法(代码更简洁)。

位置提示(地理路由)

使用位置提示控制 Durable Object 的创建位置:

// 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 East

适用场景:

  • ✅ 在用户附近创建 DO(降低延迟)
  • ✅ 满足数据驻留要求(例如,欧盟用户 → weur/eeur)

限制:

  • ⚠️ 提示仅尽力而为,无法保证
  • ⚠️ 仅影响首次创建,之后的访问会使用现有位置
  • ⚠️ 无法移动现有 DO,创建后位置即固定
管辖区域限制(数据驻留)

严格执行数据位置要求:

// Create DO that MUST stay in EU
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });

// Available jurisdictions:
// - 'eu' - European Union
// - 'fedramp' - FedRAMP (US government)

适用场景:

  • 法规合规要求(GDPR、FedRAMP)
  • 数据主权要求

重要:

  • ✅ 严格执行,DO 永远不会离开管辖区域
  • ⚠️ 管辖区域限制不能与位置提示组合使用
  • ⚠️ 管辖区域外的用户会面临更高延迟

迁移 - 管理 DO 类

在以下情况下,必须进行迁移:

  • 创建新的 DO 类
  • 重命名 DO 类
  • 删除 DO 类
  • 将 DO 类转移到其他 Worker

迁移类型:

1. 创建新的 DO 类
{
  "durable_objects": {
    "bindings": [
      {
        "name": "COUNTER",
        "class_name": "Counter"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",                    // Unique identifier for this migration
      "new_sqlite_classes": [         // SQLite backend (recommended)
        "Counter"
      ]
    }
  ]
}

对于 KV 后端(旧版):

{
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["Counter"]      // KV backend (128MB limit)
    }
  ]
}

重要:

  • ✅ 新建 DO 时使用 new_sqlite_classes(最多支持 1GB 存储)
  • ❌ 无法为已部署且由 KV 提供后端的现有 DO 启用 SQLite
2. 重命名 DO 类
{
  "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"
        }
      ]
    }
  ]
}

发生的情况:

  • ✅ 现有 DO 实例会保留其数据
  • ✅ 旧绑定会自动转发到新类
  • ⚠️ 必须在 Worker 代码中导出新类
3. 删除 DO 类
{
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["Counter"]
    },
    {
      "tag": "v2",
      "deleted_classes": ["Counter"]    // Mark as deleted
    }
  ]
}

发生的情况:

  • ✅ 现有 DO 实例会立即删除
  • ✅ 所有存储内容都会删除
  • ⚠️ 无法撤销,数据会永久丢失

删除前:

  • 如有需要,导出数据
  • 更新引用此 DO 的 Workers
4. 将 DO 类转移到其他 Worker
// 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"
        }
      ]
    }
  ]
}

发生的情况:

  • ✅ DO 实例会移至新的 Worker
  • ✅ 所有存储内容都会转移
  • ✅ 旧绑定会自动转发
  • ⚠️ 必须导出目标类
迁移规则

迁移中的重要注意事项:

❌ 迁移是原子操作,无法渐进式部署

  • 部署时,所有实例会同时迁移
  • 不支持部分发布

❌ 迁移标签必须唯一

  • 不能重复使用标签
  • 标签只能追加,不能修改或删除

❌ 无法为由 KV 提供后端的现有 DO 启用 SQLite

  • 必须改为创建新的 DO 类

✅ 代码更改不需要迁移

  • 只有架构更改(新建/重命名/删除/转移)需要迁移
  • 可以自由部署代码更新

✅ 全局唯一性按账户计算

  • DO 类名称在整个账户中唯一
  • 不同 Workers 之间也必须唯一

常见模式

模式 1:限流(按用户)
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 });
}
模式 2:会话管理
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);
  }
}
模式 3:领导者选举
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);
  }
}
模式 4:多 DO 协调
// 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);
  }
}

重要规则

务必执行

✅ 从 Worker 导出 DO 类

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

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

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

✅ 为新的 DO 使用 new_sqlite_classes

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

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

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

✅ 将关键状态持久化到存储中(而不只是内存)

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

✅ 使用闹钟,而不是 setTimeout/setInterval

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

✅ 使用参数化 SQL 查询

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

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

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

❌ 创建 DO 却不添加迁移

// Missing migrations array = error

❌ 忘记导出 DO 类

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

❌ 使用 setTimeout 或 setInterval

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

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

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

❌ 逐步部署迁移

# Migrations are atomic - cannot use gradual rollout

❌ 在现有的、由 KV 支持的 DO 上启用 SQLite

// Not supported - must create new DO class instead

❌ 使用标准 WebSocket API 并期待其支持休眠

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

❌ 假设位置提示一定有效

// Location hints are best-effort only

已知问题预防

此技能可预防 15 个以上的已记录问题:

问题 #1:未导出类

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

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

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

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

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

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

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

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

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

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

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

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

ws.serializeAttachment({ userId, username });

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

错误:即使使用休眠 API,费用仍然很高 来源:https://developers.cloudflare.com/durable-objects/best-practices/websockets/ 原因:传出的 WebSocket 不支持休眠 预防措施:仅对服务端(传入)WebSocket 使用休眠

问题 #8:全局唯一性方面的误解

错误:出现意外的 DO 类名称冲突 来源:https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness 原因:每个账户中的 DO 类名称都是全局唯一的 预防措施:了解 DO 类名称在账户中的所有 Worker 之间共享

问题 #9:KV 后端的 deleteAll 不会完全删除

错误:存储未完全删除,仍持续计费 来源:https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/ 原因:KV 后端的 deleteAll() 可能只完成部分删除 预防措施:使用 SQLite 后端实现原子性 deleteAll

问题 #10:绑定名称不匹配

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

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

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

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

错误:渐进式部署受阻 来源:https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ 原因:尝试将渐进式发布与迁移结合使用 预防措施:迁移会在所有实例上以原子方式部署

问题 #13:位置提示被忽略

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

问题 #14:Alarm 重试失败

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

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

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


配置参考

完整的 wrangler.jsonc 示例
{
  "$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"
        }
      ]
    }
  ]
}

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';
  }
}

官方文档


有问题?遇到故障?

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

评论

支持 Markdown