同じ会話画面で利用者が送信を二度押した、接続が切れてクライアントが再送した、担当者と自動処理が同じ案件を更新した。AIエージェントでは、こうした競合が同じメールを二度下書きするだけでなく、二重の外部送信、異なる会話履歴への応答、状態の上書きにつながります。
結論は、会話・案件・承認待ちなど「一つだけ順番を決めたい単位」に一つのDurable Objectを割り当て、そこで実行中の操作と確定済みの結果を管理することです。ただし、Durable Objectが単一スレッドだからといって、外部APIを待つ間も自動的に競合しないわけではありません。保存する境界、外部I/Oの前後、過負荷時の応答を設計して初めて、二重実行を減らせます。
なぜAIエージェントは会話単位の調整役を必要とするのか
通常のWorkerはステートレスです。同じ会話への二つの要求が別の実行環境で進めば、両方が「まだ実行されていない」と判断し得ます。モデル呼出しだけなら重複回答で済む場合もありますが、検索、ファイル更新、メール下書き、CRM登録などのtool実行を伴うと、後から原因を分けにくい不整合になります。
Cloudflare Durable Objectsは、名前またはIDで参照されるグローバルに一意な状態付きの実行単位です。会話IDや案件IDからObjectを決めれば、そのObjectをその単位の調整役にできます。すべての利用者を一個のObjectに集めるのではありません。全社共通の単一Objectは待ち行列を大きくし、局所的な遅延を全利用者へ広げるため、Cloudflareもglobal singletonを避けるよう案内しています。
最初に「一意にする対象」を決める
Objectの名前は実装上の都合ではなく、二重実行を防ぎたい業務境界から決めます。個人チャットならtenantId:conversationId、案件処理ならtenantId:caseIdのように、テナント境界を含めます。同名Objectに異なる顧客の状態を混在させないことが、調整と分離を両立する最小の方法です。
Object内には少なくとも、操作ID、状態、開始時刻、結果の参照を保存します。同じ操作IDが再到着したら、新しいモデル呼出しを始めず、進行中または確定済みの結果を返します。ここで操作IDは、画面の再送やネットワーク再試行を同一操作として認識するための値です。利用者の入力本文をIDにせず、入口で生成した推測不能な値を使います。
保存を先に確定し、外部I/Oをロックの内側に抱え込まない
Durable Objectsでは、一つのObject内の同期JavaScriptは単一スレッドで動き、storage操作には競合を避ける仕組みがあります。しかしawait fetch()のような外部I/Oの間には、別要求が進む設計上の余地があります。したがって「実行中」の操作IDをstorageへ確定する前にモデルや外部toolを呼ぶと、二つの要求が同じ副作用へ到達し得ます。
順序は、(1)操作IDを確認する、(2)未実行ならrunningを永続化する、(3)外部呼出しを行う、(4)結果または失敗分類を永続化する、(5)必要なら次の処理を許可する、が基本です。外部送信のような副作用は、相手先にも操作IDを渡せるなら渡し、渡せない場合は送信前後の記録と人が確認できる復旧手順を用意します。Object内の保存だけで外部サービスの二重実行まで完全に止められる、と断定してはいけません。
SQLite-backed Durable ObjectsのstorageはObjectごとにprivateで、強整合・トランザクション的な操作を提供します。新しいnamespaceにはSQLite backendが推奨されています。一方、cursorをawaitをまたいで使うとsnapshot isolationを期待できないため、検索結果は外部I/O前に読み切るか、あらためて状態を検証します。
休止・再起動を「例外」ではなく通常経路として扱う
Durable Objectは休止や再起動でメモリから消え、次の要求でconstructorが再び動きます。WebSocketを休止しても接続を保つ仕組みはありますが、メモリ上の会話状態は復元されません。会話履歴の全件を常時Objectへ置く必要はないものの、操作ID、進行状態、承認期限、最後に確定した結果の参照のように、復帰に必要な状態はstorageへ書きます。
shutdown hookは提供されないため、「最後にまとめて保存する」設計は脆弱です。進捗を小さく確定し、再開時にはrunningのまま残った操作を、期限・外部結果・利用者への表示の三点から判定します。無条件再実行は避けます。外部送信済みか不明な操作はfail-close(安全側に明確に止める)し、人が再開判断できる状態にします。
過負荷は再試行で押し切らない
一つのDurable Objectには処理能力の上限があり、過負荷ならキューイング後にoverloaded errorを返すことがあります。この場合、同じObjectへ直ちに再試行を重ねると、待ち行列とエラー率を悪化させます。Cloudflareのエラー処理指針も、overloadedと印されたエラーを再試行しないよう示しています。
利用者には「処理中」「混雑のため開始できない」「再実行可能」のどれかを明確に返し、前者は保存済み操作IDで照会できるようにします。重い一括処理は会話Objectへ閉じ込めず、Queuesの再試行・DLQ設計やWorkflowsの長時間処理へ分けます。Durable Objectは調整役であり、すべての計算を抱える実行基盤ではありません。
実装前チェックリスト
- Object名にテナント境界と会話または案件の一意な単位を含めたか
- 同一操作を判定する操作IDを入口で発行し、本文や個人情報を含めていないか
running状態を外部モデル・tool呼出しより前に永続化するか- 外部副作用の操作ID、確認結果、復旧担当を記録するか
- 再起動後に必要な状態をメモリだけへ置いていないか
- overloaded errorを即時再試行せず、利用者に明確な状態を返すか
- 重いバッチ・再試行・長時間待機をQueuesまたはWorkflowsへ逃がすか
- request ID・operation ID・Object IDを障害追跡の設計で相関できるか
限界:単一スレッドは万能な排他制御ではない
Durable Objectsは、会話や案件のような小さな共有状態を調整する有力な選択肢です。しかし、外部APIの正確に一度だけの実行、複数Objectをまたぐ原子的な処理、長時間のモデル計算、全社横断の集計を自動的に解決するものではありません。単一Objectに顧客全体の処理を集めると、可用性と遅延の単一障害点にもなります。
必要なのは、会話単位の順序をObjectで決め、外部副作用には冪等性と確認手順を足し、長い仕事は別の非同期基盤へ出すことです。これにより、単純な再送が顧客対応上の事故へ育つ経路を小さくできます。
AI基盤設計・セキュリティ相談
AIエージェントの会話状態、同時実行、非同期処理を本番設計へ落とし込みたい方は、AI基盤設計・セキュリティ相談をご利用ください。
一次情報と確認範囲
確認日:2026年9月8日。以下はCloudflare Durable Objectsの現行ドキュメントを対象にした設計整理であり、利用プラン・制限・互換性日は導入時に再確認してください。
| 機関・資料 | 確認した範囲 | URL |
|---|---|---|
| Cloudflare | stateful coordination、input/output gates、global singletonを避ける指針 | Rules of Durable Objects |
| Cloudflare | Object単位の単一スレッド性、overloaded error、CPU・storage制約 | Durable Objects Limits |
| Cloudflare | 休止・再起動、永続化、shutdown hookがないこと | Lifecycle of a Durable Object |
| Cloudflare | SQLite storageの強整合性、private storage、cursorの注意点 | SQLite-backed Durable Object Storage |
| Cloudflare | retryable errorとoverloaded errorの扱い | Error handling |
| Cloudflare | WebSocket休止とメモリ状態の復元要件 | Use WebSockets |
一次情報と確認範囲
確認日:2026年9月8日。製品仕様・制度は更新されるため、導入時はリンク先の最新版を再確認してください。海外の制度・職業規範は日本へ直接適用せず、相違点と限界を本文に明記しています。
