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 Hibernation、Alarms APIを使った保存、通信、タスク実行、呼び出しまでを扱います。

主な機能

  1. 01Durable Objectsプロジェクトの作成
  2. 02SQLiteとキーバリューストレージの利用
  3. 03WebSocket Hibernationによる接続処理
  4. 04Alarms APIによるタスク予約
  5. 05RPCまたは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

Status:本番環境対応 ✅ 最終更新日: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 Hibernation API により、1 インスタンスあたり数千の接続に対応します
  • 永続的な状態 — 組み込みの 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
  • DO クラスの実装例
  • DO を呼び出す 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 は使用しないでください — ハイバネーションを妨げます(代わりにアラームを使用してください)
  • ❌ 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>;
}

State API — 永続ストレージ

Durable Objects は、バックエンドに応じて2 つのストレージ APIを提供します。

  1. SQL API(SQLite バックエンド)— 推奨
  2. Key-Value API(KV または SQLite バックエンド)
SQLite バックエンドを有効にする(推奨)

wrangler.jsonc のマイグレーションで設定します。

{
  "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 の重要なルール:

  • ✅ ? プレースホルダーを使ったパラメーター化クエリを必ず使用します
  • ✅ 頻繁にクエリされる列にはインデックスを作成します
  • ✅ 複数ステートメントの操作にはトランザクションを使用します
  • ❌ 内部で KV API に使用される、非公開の __cf_kv テーブルにアクセスしないでください
  • ❌ KV バックエンドを使用する既存のデプロイ済み DO で SQLite を有効にしないでください(サポートされていません)
Key-Value API

ctx.storage を通じて、SQLite と KV の両バックエンドで利用できます。

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 バックエンド:DO インスタンスあたり最大 1GB のストレージ
  • KV バックエンド:DO インスタンスあたり最大 128MB のストレージ

WebSocket ハイバネーション API

WebSocket ハイバネーション API を使用すると、Durable Objects は次のことができます。

  • インスタンスごとに 数千の WebSocket 接続を処理する
  • アイドル状態(メッセージもイベントもない状態)のときに ハイバネーションして、コストを節約する
  • メッセージの到着時に自動的に 復帰する
  • アイドル期間中に継続時間の料金を発生させずに接続を維持する

用途: チャットルーム、リアルタイムコラボレーション、マルチプレイヤーゲーム、ライブ更新

ハイバネーションの仕組み
  1. アクティブ状態 - DO がメモリ上にあり、メッセージを処理しています
  2. アイドル状態 - 約 10 秒間メッセージがないと、DO はハイバネーションできます
  3. ハイバネーション - メモリ内の状態はクリアされますが、WebSocket は Cloudflare のエッジに接続されたままです
  4. 復帰 - 新しいメッセージが到着 → コンストラクターが実行 → ハンドラーメソッドが呼び出されます

重要: メモリ内の状態はハイバネーション時に失われます。WebSocket ごとのメタデータを永続化するには、serializeAttachment() を使用してください。

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 を await している場合)
  • 標準の WebSocket API を使用している場合(ハイバネーション API ではない場合)
  • リクエスト/イベントの処理がまだ完了していない場合

Alarms API - スケジュールされたタスク

Alarms API を使用すると、Durable Objects は将来の特定の時刻に自らを起動するようスケジュールできます。

用途: バッチ処理、クリーンアップジョブ、リマインダー、定期タスク、遅延操作

基本的なアラームのパターン
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);
  }
}

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

アラームの動作:

  • ✅ 少なくとも 1 回の実行を保証 - 失敗時には再試行されます
  • ✅ 自動再試行 - 指数バックオフ方式で最大 6 回再試行します(初回は 2 秒後)
  • ✅ 永続的 - DO のハイバネーションやエビクション後も維持されます
  • ✅ 実行成功後に自動削除
  • ⚠️ DO ごとに 1 つのアラーム - 新しいアラームを設定すると、以前のアラームは上書きされます

再試行のパターン(べき等な操作):

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 は2 つの呼び出しパターンをサポートしています。

  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 の作成方法は 3 つあります。

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 ms かかります
  • ⚠️ 初回使用後はキャッシュ - 以降のアクセスは高速です
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
  • 自然な名前がない場合

特性:

  • ✅ 初回使用時のレイテンシが低い(グローバルな一意性チェックが不要)
  • ⚠️ 後で同じ DO にアクセスするには、ID を保存する必要があります
  • ⚠️ 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 を作成する(レイテンシを低減)
  • ✅ データレジデンシー要件がある場合(例:EU のユーザー → 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 を参照する Worker を更新します
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 クラス名はアカウント全体で一意です
  • Worker が異なる場合も同様です

一般的なパターン

パターン 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/ 発生する理由:DO クラスが Worker からエクスポートされていない 防止策:

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_sqlite_classes の代わりに new_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/ 発生する理由:ハイバネーション時にクリアされるメモリ内の状態に依存していた 防止策:WebSocket のメタデータには serializeAttachment() を使用する

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() は部分的に失敗することがある 防止策:アトミックな deleteAll を実現するには、SQLite バックエンドを使用する

Issue #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
Issue #11:状態サイズの上限超過

エラー:"state limit exceeded" またはストレージエラー 参照元:https://developers.cloudflare.com/durable-objects/platform/pricing/ 発生理由:1GB(SQLite)または 128MB(KV)の上限を超過しました 防止策:ストレージサイズを監視し、アラームを使ってクリーンアップを実装してください

Issue #12:マイグレーションがアトミックではない

エラー:段階的デプロイがブロックされます 参照元:https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ 発生理由:マイグレーションで段階的ロールアウトを試みました 防止策:マイグレーションはすべてのインスタンスに対してアトミックにデプロイされます

Issue #13:ロケーションヒントが無視される

エラー:DO が誤ったリージョンに作成されます 参照元:https://developers.cloudflare.com/durable-objects/reference/data-location/ 発生理由:ロケーションヒントはベストエフォートであり、保証されません 防止策:厳密な要件には管轄区域を指定してください

Issue #14:アラームの再試行に失敗する

エラー:アラームの失敗後にタスクが失われます 参照元:https://developers.cloudflare.com/durable-objects/api/alarms/ 発生理由:アラームハンドラーが繰り返しエラーをスローします 防止策:冪等性のあるアラームハンドラーを実装してください

async alarm(info: { retryCount: number }): Promise<void> {
  if (info.retryCount > 3) {
    console.error('Giving up after 3 retries');
    return;
  }
  // Idempotent operation
}
Issue #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. マイグレーションの設定を慎重に確認してください

コメント