D1に入れてはいけないデータがある:Cloudflare KVとデータ寿命設計の答えのサムネイル

はじめに

セッション管理の実装でこんなことありませんか?

D1でユーザーテーブルを作り、認証の土台ができた。次の課題はセッションの保存先。自然な流れでセッションテーブルをD1に作り始めた。しかしリクエストのたびにSQLクエリが走り、期限切れセッションの削除バッチが必要になり、気づけばセッション管理だけで3つの問題を抱えることになった。RedisやMemcachedを使いたいが、Cloudflare Workers環境でどう調達するか見当もつかない。

この記事をお勧めしない人

  • Cloudflare Workers以外のプラットフォームで開発しており、移行を検討していない人。
  • セッション管理にすでにRedisやDB直接書き込みが確立しており、運用コスト上の問題も感じていない人。
  • 強一貫性(Strong Consistency)が必須のユースケースで、最終的一貫性を受け入れられない人。

もし一つでも当てはまらないなら、読み進める価値があるかもしれません。

D1にセッションを置くと静かに崩れていく

  • セッションテーブルが肥大し始める。TTLの自動管理ができないため、期限切れレコードが蓄積し続け、ストレージを侵食する。
  • 毎リクエストのSELECTクエリがレイテンシを積み上げる。セッション確認がページ表示のボトルネックになり始める。
  • TTL削除のcronジョブが必要になる。認証の「本体」ではない仕事が増え、運用の複雑さが認証の正確さを蝕む。

データ寿命を理解した設計ならこうなる

  • TTL付きの1行でセッションを保存すれば、期限切れデータはCloudflareが自動で削除する。cronジョブは不要。
  • KV BindingはD1同様HTTP不要のRuntime直結。セッション確認がリクエストのボトルネックにならない構成を得られる。
  • この設計はClaudeMixの認証フロー全体——セッション・OTPトークン・レートリミット・パスワードリセットトークンを支える実稼働構成から持ち帰れる。

このブログもD1にセッションを置きかけた

D1でユーザーDBを作った後、次のステップとしてセッションテーブルをD1に実装し始めた経緯があります。TTLの問題に直面してKVに切り替えた結果、セッション管理のコードが大幅にシンプルになりました。この記事では、その判断の根拠とClaudeMixの実装パターンを持ち帰れるように書きました。D1の記事と合わせて読むと、2つのストレージの役割分担が明確になります。

延命医の診断

Key-Value思想の普遍性

Key-Valueという設計は1960年代のUnixキャッシュから始まり、DNSキャッシュ・memcached・Redisへと続く。構造は常に同じで「キーで値を引く、値は一時的に消えてもいい」。Cloudflare KVはこの50年以上生き続けた思想をエッジに持ち込んだものだ。実験技術ではない。

2024〜2025年のKV再設計

Cloudflareは2024〜2025年にKVの内部アーキテクチャを作り直した。結果としてp99読み取りレイテンシが200ms超から数ms〜数十msへ大幅に改善された。かつての「KVは遅い」という批判はもはや古い情報。外部RedisへのHTTP呼び出しより高速で、セッション管理のような高頻度読み取りに十分な性能を持つ。

Workers Binding直結

D1と同じく、KVはHTTPを介さずWorkers RuntimeにBindingとして直結する。外部Redisを使えばWorkers外へのHTTP呼び出しが発生するが、KVはそれを排除する。Edge runtime → Edge storageの直結がセッション確認を最速にする。

選定理由

Workers連動の根拠

KVは独立した選択というより、Cloudflare Workersという実行環境を選んだ時点で自然に選択肢に入ってくるストレージだ。ただし、「使えるから使う」ではなく「セッション管理というユースケースと構造的に合致している」という積極的な理由がある。

セッションデータは「存在時間が短い」「履歴が不要」「頻繁に読まれる」という3つの性質を持つ。KVのTTL・KeyValueシンプルアクセス・エッジキャッシュはこの3性質に対してそれぞれ答えを持つ。

データ寿命モデル

この判断の根底にあるのはデータ寿命の概念だ。

permanent state(履歴が価値になる)
  User / Subscription / Article
  → D1

temporal state(存在に寿命がある状態)
  Session / OTP / RateLimit / PasswordResetToken
  → KV

live coordination state(リアルタイム同期が必要な状態)
  WebSocket / チャットルーム / 分散ロック
  → Durable Objects

Cloudflareのstateレイヤーはこの3層で完結している。D1・KV・Durable ObjectsはそれぞれEdge OSのrelational/temporal/live stateに対応する。本記事が扱うのは中間層のKVだ。

KVは「軽いDB」でも「cacheレイヤー」でもない。【time-indexed state store】——データ寿命の管理責任をアプリから奪い、ストレージ側に委ねるという設計思想を持つ。普通のDBはアプリがcreate→read→expire→deleteの全工程を管理する。KVはput(key, value, ttl)の1行で終わる。バグの半分は寿命管理で起きる。KVはその責任ごと引き取る。

なぜ代替案を選ばなかったか

技術 不採用理由
D1(セッションテーブル) TTLの自動管理ができない。期限切れレコードの削除バッチが必要になる。毎リクエストのSQLクエリがセッション確認には過剰なオーバーヘッドになる。
外部Redis(Upstash等) Workers外部へのHTTP呼び出しが発生しレイテンシが増加する。Workers Bindingによる直結の恩恵を得られない。
Durable Objects 強一貫性が必要なユースケース向けの設計で、コストが高い。セッション管理の最終的一貫性で十分な要件に対してオーバースペック。

構造的メリット

  1. TTL自動削除でcronゼロ: expirationTtlを指定するだけで、Cloudflareが期限切れデータを自動削除する。セッション・OTP・レートリミットカウンターのすべてで削除バッチの実装が不要になる。

  2. 操作がget/put/deleteの3種類: SQLの複雑なクエリやトランザクションが不要。操作の状態空間が小さいほどAIは壊しにくい——KVの有限操作系は「AIに任せても壊れない」設計と直結する。

  3. 4つの認証ユースケースを1つのBindingで管理: セッション・OTPトークン・レートリミット・パスワードリセットトークン。すべてTTL付き・短命・高速読み必要という同一パターンで、SESSION_KVという単一のBindingで管理できる。

ClaudeMixでの活用例

ClaudeMixではSESSION_KVという名前でKVをバインドしている。4つの認証ユースケースすべてがこのBindingを使う。

# wrangler.toml
[[kv_namespaces]]
binding = "SESSION_KV"
id = "your-kv-namespace-id"

セッション保存(TTL付き)

// app/data-io/account/common/saveSession.server.ts
const kv = env.SESSION_KV;

// userIdをキーに含めることで、ユーザー単位の一括削除をO(1)で実現
const kvKey = `session:${sessionData.userId}:${sessionData.sessionId}`;

await kv.put(kvKey, JSON.stringify(sessionData), {
  expirationTtl: ttlSeconds, // 有効期限をTTLで渡すだけ。削除バッチは不要
});

OTPトークン保存(短命TTL)

// app/data-io/account/authentication/saveOtpToken.server.ts
const kvKey = `otp:${userId}`;

await kv.put(kvKey, JSON.stringify({ token, expiresAt }), {
  expirationTtl: 600, // 10分。用が済めばCloudflareが自動削除
});

レートリミットカウンター

// app/data-io/account/authentication/checkOtpRateLimit.server.ts
const kvKey = `ratelimit:otp:${userId}`;
const current = await kv.get(kvKey, 'json');

await kv.put(kvKey, JSON.stringify({ count: (current?.count ?? 0) + 1 }), {
  expirationTtl: 3600, // 1時間経過で自動リセット。cronなし
});

4つすべてのユースケースに共通するパターンがある。TTL付きのput1行で書き込み、期限管理はCloudflareに委ねる。これがKVとD1の本質的な役割分担だ。

公式リファレンス・更新履歴

セッション・OTP・レートリミット——すべてKVで管理できた。しかしOTPトークンを保存するだけでは認証メールは届かない。メール送信という外部サービスとの接続が必要になった時、Cloudflare完結設計に初めて「外」との接続が生じる。これをどう設計するかが次の問いだ。

あなたのリポジトリへの適用

どちらを採用しますか?(1つ選択)

SEPARATION|責務を分離

セッションデータ(KV)と永続データ(D1)の境界が実装で守られているか確認する

調査:

  1. セッション情報が全てKVで管理されており、D1には保存されていないか確認せよ
  2. KVに保存すべきでない永続データ(ユーザープロフィール等)がKVに混入していないか確認せよ
  3. KVのkeyのプレフィックス規則が一貫して適用されているか確認せよ

この内容をClaude Codeに貼り付けて実行します

SIMPLICITY|シンプルに管理

TTL削除のためのcronジョブが不要なKV設計になっているか確認する

調査:

  1. セッションのTTL削除がKVのTTL機能(expirationTtl)で自動的に行われているか確認せよ
  2. D1でcronを使ったTTL削除が残っていないか確認せよ
  3. KVのTTL設定値がセッション有効期間の要件と一致しているか確認せよ

この内容をClaude Codeに貼り付けて実行します

外部コードのローカル実行にはリスクがあります。ブラウザ環境での実行を推奨します。