ソース修正済み / 本番停止中
$666 の Durable Objects 請求はなぜ発生したか
何も処理していなくても、全履歴を書き直していた。
ぬこぬこさんが automated-hachathon-judge の JudgeState でやらかした、全件再保存の実装事故です。Cloudflare 請求書の Rows Written $666 明細をきっかけに、増幅経路を確認して差分保存と日次上限で塞ぐまでを公開します。
Cloudflare 請求書は Cloudflare アカウント全体の合算です。$666 全額を JudgeState Durable Object namespace へ帰属できる過去メトリクスはありません。
まず結論
- 請求書の Rows Written
- 665,418,208 行
- 旧 Runner の名目送信頻度
- 約 5.2 回 / 秒
- 新しい日次書き込み上限
- 50,000 行
前提
Durable Objects とは
Cloudflare Durable Objects は、チャットルームや対戦部屋など、特定の対象に関する処理とデータをまとめて管理する仕組みです。同じ ID へのリクエストを同じ Object に集め、専用のストレージに状態を保存できます。複数の利用者が参加するチャットやゲーム、共同編集などで使われます。[12]
たとえば対戦ゲームなら、部屋ごとに Object を用意し、参加者の操作を受け取ってゲームの状態を更新し、その結果を全員に送ります。データの保存に加えて、そのデータを扱う処理や参加者の接続も同じ場所で管理できるのが特徴です。
他の Cloudflare サービスとの違い
Workers が Web サイトや API の処理を実行し、D1 が SQL で扱うデータを保存するのに対して、Durable Objects は対象ごとの状態と処理を受け持ちます。KV は読み取り中心のデータ、R2 は画像などのファイル、Queues は後で実行する仕事の受け渡しを担当します。[13]
今回の審査アプリでは、審査待ちのジョブや処理状況を JudgeState という Durable Object に集めていました。複数の Runner(審査を実行するプログラム)の処理枠が同時に仕事を取りに来ても、同じジョブを重複して渡さないためです。[3]
前提
Durable Objects の課金体系
料金は処理とストレージに分かれます。以下は 2026 年 9 月 10 日に確認した SQLite ベースの Durable Objects の料金です。Workers Paid は月額最低 $5 で、含まれる利用枠を超えると従量料金が加わります。呼び出し元の Worker の利用料は別に計測されます。[2]
- リクエスト数
- Free: 10 万回 / 日。Paid: 100 万回 / 月まで含み、超過 100 万回につき $0.15。
- 稼働時間(Duration)
- Free: 13,000 GB-s / 日。Paid: 400,000 GB-s / 月まで含み、超過 100 万 GB-s につき $12.50。
- 読み取り行数
- Free: 500 万行 / 日。Paid: 250 億行 / 月まで含み、超過 100 万行につき $0.001。
- 書き込み行数
- Free: 10 万行 / 日。Paid: 5,000 万行 / 月まで含み、超過 100 万行につき $1。
- 保存容量
- Free: 合計 5 GB。Paid: 5 GB-month まで含み、超過 1 GB-month につき $0.20。
Free は上限を超えた種類の操作がエラーになり、日次枠は UTC 0 時にリセットされます。Paid の超過利用量は課金単位へ切り上げられます。上記は SQLite ベースの料金で、旧 KV ベースのストレージには別の料金体系があります。
稼働時間と書き込みは別に数える
GB-s は割り当てメモリ容量と経過時間の積です。Duration は実際のメモリ使用量によらず Object ごとに 128 MB として計測され、CPU が計算した時間だけを指すものではありません。休止可能な待機中は対象外ですが、休止できない状態では待機中も対象になります。
SQLite ベースでは put() と delete() も書き込み行数に含まれます。保存容量が増えなくても、同じデータを書き直し続けると書き込み量は積み上がります。今回の $666 は、この Rows Written の請求明細です。[1]
1
請求書に現れた 6.6 億行
2026 年 7 月 4 日〜8 月 3 日の Cloudflare 請求書には、Durable Objects Storage Rows Written が 665,418,208 行、金額は $666 と記載されていました。[1]
Rows Written は保存容量ではありません。storage.put または storage.delete が処理した key の数です。1 回の処理で 100 個の key を書けば、100 行として数えられます。[2]
この請求書は Cloudflare アカウント内の全 Durable Object namespace を合算しています。namespace 別の過去メトリクスがないため、hackathon-nukoevi-app の JudgeState と influ-quest の寄与を分離できません。
2
Durable Object を使った理由
このレポートで確認した 2 つのアプリでは、審査ジョブや利用者ごとの状態を管理するために Durable Objects を使っていました。
automated-hachathon-judge
JudgeState は、審査キュー、処理中ジョブ、審査結果を保存します。Runner(審査を実行する常駐プログラム)の複数の処理枠が同時に仕事を取りに来ても、同じジョブを重複取得しないためです。[3]
Durable Object の採用は要件に合っていました。問題は、高頻度の定期通信と全件保存をつないだことです。
3
1 回の通信が全件保存になった
旧 Runner は仕事がない時間も JudgeState へ通信し、JudgeState はそのたびに保存済みジョブ全体を書き戻していました。
-
空のキューを何度も確認する
10 個の処理枠が 2 秒ごとに claim を送り、5 秒ごとに heartbeat を送ります。旧 Runner の既定値では、合計の名目頻度が約 5.2 回 / 秒でした。[4]
-
変化がなくても保存する
空振りの claim と heartbeat の後にも、JudgeState は meta key と保存済みの全 job key を書き戻していました。[5]
-
保存件数だけが増え続ける
「最近の投稿」は 50 件までの表示でしたが、保存する job key には上限がありませんでした。ジョブが増えるほど、1 回の通信で書く行数も増えます。[6]
概算 Rows Written ≈ 保存頻度 ×(保存済み job key 数 + meta key 1 件)× 稼働時間
5.2 回 / 秒 × 51 行 / 回 × 86,400 秒 / 日 = 22,913,280 行 / 日
書き込み増幅の大きさを示す計算であり、$666 を再現した実測値ではありません。
4
全件保存をやめ、二重に止めた
JudgeState は、値が変わった key だけを保存するように変更しました。異常へ早く気づく通知と、自動停止する上限も追加しています。[9]
- 状態が変わらない処理
- 修正前meta key と保存済みの全 job key を再保存
- 修正後空のキューを確認する claim などは put と delete が 0 件
- 状態が変わる処理
- 修正前変更していない job key まで再保存
- 修正後値が変わった job key と meta key だけを保存
- 通信と入力
- 修正前2 秒間隔の claim と、無認証の scoring callback
- 修正後空振り時は最大 30 秒まで待機し、callback は認証と入力を検証
5
この失敗から持ち帰ること
今回のポカは Durable Object を選んだことではなく、高頻度の定期処理と全件保存をつないだことでした。
-
変化がなければ書かない
状態を変えないリクエストは、永続ストレージへの put と delete を 0 件にします。
-
回数だけでなく 1 回の重さを見る
費用は「通信頻度 × 1 回に書く key 数 × 稼働時間」で見積もります。
-
通知だけで終わらせない
書き込み件数をテストし、保存件数に上限を設け、異常時はアプリ側でも止めます。
資料
出典
- Cloudflare 請求書の Durable Objects Storage Rows Written 明細(2026 年 7 月 4 日〜8 月 3 日)
- Cloudflare Durable Objects Pricing
- automated-hachathon-judge の構成と Durable Object の役割
- automated-hachathon-judge commit 8b494bd の Runner 並列数、claim 間隔、heartbeat
- automated-hachathon-judge commit 8b494bd の JudgeState 無条件保存と全 job key 再保存
- automated-hachathon-judge commit 8b494bd の recentIds 50 件上限と上限のない jobs 保持
- influ-quest commit 9c518da の QuestAgent と BrowserChatSession Durable Object binding
- influ-quest commit 9c518da の BrowserChatSession によるセッション単位の状態保存とリクエスト直列化
- automated-hachathon-judge commit c2ffc4a の差分保存、claim backoff、scoring callback 検証
- Cloudflare Budget Alerts
- automated-hachathon-judge commit e376e44 の日次書き込みガードと HTTP 507 による停止
- Cloudflare: What are Durable Objects?
- Cloudflare: Choosing a data or storage product