Durable Objects 運用 — Pilot: RateLimiterDO
監査 P1 (A6 / 2026-04-26 / Phase 30) で導入した最初の DO。Cloudflare Rate Limiting binding (RATE_LIMIT_USER 等) では難しい (user × resource) 複合 scope の sliding window 制御を担う。
全体構成
Worker fetch handler
└─ checkRateLimitDO(env, { scope, keys, limit, windowSeconds })
│
└─ DurableObjectNamespace.idFromName(`${scope}|${k1}=${v1}|${k2}=${v2}`)
│
└─ RateLimiterDO instance (state: { hits[], limit, windowSeconds })
└─ POST /check → 429 if limit exceeded, 200 otherwise
(X-RateLimit-Remaining / X-RateLimit-Reset ヘッダ付与)
- インスタンス名は scope + keys のタプルから決定論的に組立。同 scope のリクエストは必ず同 instance に集約される (atomic counter 性が確保される)。
state.hitsは memory + storage に保持。コールドスタート復元あり。
binding 定義
wrangler.toml と 3 split file (wrangler.{admin,marketing,public}.toml) すべてに:
[[env.dev.durable_objects.bindings]]
name = "RATE_LIMIT_DO"
class_name = "RateLimiterDO"
[[env.dev.migrations]]
tag = "v1-rate-limiter-do"
new_sqlite_classes = ["RateLimiterDO"]
new_sqlite_classes は新世代 SQLite-backed DO (Cloudflare 推奨)。古い new_classes ではなくこちらを使う。
DO クラスはメインモジュールから export する必要がある。各 entrypoint:
- parky/api/src/index.ts
- parky/api/src/index-admin.ts
- parky/api/src/index-marketing.ts
- parky/api/src/index-public.ts
すべて末尾に export { RateLimiterDO } from "./durable-objects/rate-limiter"; を持つ。
helper
parky/api/src/lib/rate-limit-do.ts
import { checkRateLimitDO } from "../../lib/rate-limit-do";
const result = await checkRateLimitDO(c.env, {
scope: "notif.send",
keys: { user_id: userId, target_lot: lotId },
limit: 3,
windowSeconds: 30,
});
if (!result.allowed) {
throw rateLimited("通知送信が短時間に集中しています");
}
binding RATE_LIMIT_DO 未設定 → fail-open (always allow + fallback: true)。dev / preview の救済用。
既存 RATE_LIMIT_USER との使い分け
| 観点 | RATE_LIMIT_USER (binding) | RateLimiterDO (pilot) |
|---|---|---|
| 設定 | wrangler [[unsafe.bindings]] |
DO binding + migration |
| scope 次元 | 単一 (key 文字列) | 複合 (scope + multiple keys) |
| Window 制御 | fixed (10/60s) | sliding (動的) |
| 動的 limit | 静的 | リクエストパラメータで上書き可 |
| コスト | 無料 | DO 課金 (1M req/月 = $0.20) |
| 推奨用途 | AI 検索の単一ユーザー上限 | per-user × per-resource の細粒度 |
順次拡張先: 通知送信の per-user×per-target / コメント投稿の per-user×per-thread / 設定変更の per-user×per-resource など。
デプロイ
DO の migrations は Worker deploy 時に Cloudflare 側に登録される。初回 deploy で RateLimiterDO クラスが認識される。
wrangler deploy --env dev
# → "Triggered new migration: v1-rate-limiter-do"
prod 投入時の注意:
- 既存 DO に対する破壊的変更 (rename / class 削除) は新しい migration tag (例
v2-...) でrenamed_classes/deleted_classesを指定。 - 同じ tag の重複適用はエラー。
トラブルシュート
DO instance が反応しない
wrangler tail --env devでログ確認- migration tag が dev / prod で揃っていない場合 deploy がエラー終了
hits が想定と違う
- インスタンス名が違う可能性 → caller 側
keysを確認 (Object.entries().sort()で deterministic) - 異なる instance に分散していないか peek:
RATE_LIMIT_DO.idFromName(name)の name を直接出力して比較
コスト
- DO の writeDataPoint と異なり、storage put 含むので実 transaction がカウントされる
- 無料 tier 1M / 月。Parky の現状トラフィックなら余裕
ロードマップ
| Wave | 用途 | scope |
|---|---|---|
| W1 (今回) | 通知送信 burst 防止 | notif.send × user × target |
| W2 | コメント投稿 spam 防止 | comment.post × user × thread |
| W3 | Live Presence (WebSocket) | presence × room |
| W4 | Idempotency cache (KV alternative) | idempotency × method × key |