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:

すべて末尾に 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
↗ Source markdown (durable-objects.md)