言語を切り替える
テーマを切り替える

Cloudflare Workers KV 実践ガイド:分散型キーバリューストレージ入門から精通まで

深夜3時、Cloudflare ダッシュボードのレイテンシーグラフを凝視していました。赤いラインがまだ 200ms を超えています。Workers を使っているし、コードも十分に最適化したはずなのに、なぜ各ユーザーリクエストがこれほど待たなければならないのでしょうか?

原因はデータベースにありました。各セッションクエリが、エッジノードからヨーロッパのデータセンターへ往復していたのです。Workers の実行時間が 5ms でも、ネットワーク転送で時間を使い果たしていました。

その後、Workers 自体はステートレス(状態を持たない)であることを理解しました。本当に「エッジに住む」ストレージが必要だったのです。それが Cloudflare Workers KV です。

この記事では、私が経験した失敗、測定したデータ、書いたコードを余すところなく共有します。KV とは何か、なぜ sub-10ms(10ミリ秒以下)のレイテンシーを実現できるのか、Session Storage と API Cache の完全実装まで詳しく解説します。さらに、いつ KV を使い、いつ D1 や R2 が適切かという選択基準も紹介します。この選択は実は非常に重要です。

KV とは — 分散型 Edge ストレージの理解

シンプルに言えば、KV は Cloudflare が Workers に用意した「携帯メモリ」です。このメモリは特定のデータセンターに置かれるのではなく、世界中の 300 以上のエッジノードに分散配置されます。東京のユーザーリクエストなら、データは東京のエッジノードで待機しているかもしれません。フランクフルトのリクエストなら、フランクフルトのキャッシュに既に存在している可能性があります。

Cloudflare Workers KV は、エッジコンピューティング向けに設計されたグローバル分散型キーバリューストレージです。主要な特徴は3つあります。

超高速読み取り。ホットキー(頻繁にアクセスされるキー)のキャッシュヒット時のレイテンシーは 500µs から 10ms の間です。正直、最初この数字を見たときは疑いましたが、自分でベンチマークを走らせてみると、確かに一桁ミリ秒レベルで安定していました。

グローバルレプリケーション。データを書き込むと、世界中のすべてのエッジノードにコピーされます。これは Redis クラスターとは異なり、KV のデータモデルは「一度書けば、どこでも読める」です。読み取り多めのシナリオに適しています。

高スループット。単一キーの読み取りは数千 RPS(requests per second)に達します。データがエッジキャッシュにあるため、毎回オリジンに戻る必要がありません。

500µs - 10ms
ホットキーレイテンシー範囲

Cloudflare ストレージファミリー比較

KV は Cloudflare ストレージマトリックスの一部です。全体像を見てみましょう。

ストレージサービスデータモデル最適なシナリオ書き込み制限レイテンシー特性
KVKey-ValueSession、Cache、設定1 RPS/keyホットキー 500µs-10ms
D1SQL(SQLite)ユーザーデータ、注文、レポートハード制限なし場所による、通常 50-200ms
R2Object Storageファイル、画像、動画ハード制限なしダウンロード高速、アップロードはファイルサイズ依存
Durable Objectsステートフルオブジェクト協力編集、WebSocketハード制限なし特定ノードへの配置が必要

この表を見て、「1 RPS/key って何?」と思われるかもしれません。これについては後で詳しく説明します。簡単に言うと、各キーは毎秒1回しか書き込めないという、KV で最も注意すべき制限です。

KV 適用シナリオ早見表

いつ KV を検討すべきでしょうか?簡単な判断基準をお伝えします。

KV が推奨される場合

  • Session storage(ユーザーログイン状態)
  • API response cache(サードパーティ API の戻り値)
  • Rate limiting counters(レート制限カウンター)
  • Feature flags / 設定データ
  • Redirect mapping(URL リダイレクトルール)

KV が推奨されない場合

  • 頻繁な書き込みが必要なデータ(リアルタイムカウンターなど、1 RPS を超える場合)
  • SQL クエリが必要な複雑なデータ(ユーザーテーブル、注文テーブルは D1 を使用)
  • 大容量ファイルストレージ(画像、動画は R2 を使用)
  • 強整合性が必要な金融取引データ(Durable Objects を使用)

Cloudflare 公式ドキュメントにも明記されています。KV は「高読み取り率、低更新頻度、即時整合性が不要」なシナリオに適しています。OpenAuth などの認証フレームワークも、デフォルトで KV をセッションストレージとして使用しています。後ほど完全な実装コードを紹介します。

KV アーキテクチャ詳細解析 — なぜこれほど高速なのか

KV の速度は魔法ではなく、3層キャッシュアーキテクチャの結果です。

コンビニで買い物をする状況を想像してください。理想的なのは、商品がカウンターの横の棚にあり、手を伸ばせば取れる状態です(エッジキャッシュ)。少し劣るのは、商品が倉庫にあり、店員が取りに行く場合です(リージョナルキャッシュ)。最も遅いのは、商品が総倉庫にあり、トラックで届くのを待つ場合です(セントラルストア)。

KV のアーキテクチャはこの3層構造です:

リクエスト → Edge Cache(最速)
             ↓ ミス
           Regional Cache
             ↓ ミス
           Central Store(最遅)

Cloudflare の 2025年10月のブログデータによると、約30%のリクエストがキャッシュ層で直接解決されます。これは3回に1回の読み取りがセントラルストレージまで戻る必要がないことを意味し、レイテンシーは自然と下がります。

30%
エッジキャッシュヒット率

パフォーマンスデータ:公式から実測まで

Cloudflare 公式ドキュメントは以下の参考データを提供しています:

  • ホットキー(頻繁にアクセスされるキー):500µs から 10ms
  • コールドキー(初回アクセスまたは稀なアクセス):レイテンシーが高く、オリジンへのアクセスが必要

正直、「500µs」という数字は最初あまり信じていませんでした。その後、自分でテストしてみました:

// シンプルなレイテンシーテストコード
const start = Date.now();
await env.KV.get("test-key");
const latency = Date.now() - start;
console.log(`Latency: ${latency}ms`);

100回テストした結果、ホットキーの平均レイテンシーは確かに 5-8ms 程度でした。コールドキーは初回アクセスで 50ms を超えますが、2回目のアクセスで低下します。キャッシュが効いたのです。

2025年に Cloudflare は KV の大規模な改修を行い、公式ブログデータによると、操作速度が3倍に向上しました。主な変更点は2つ:

  1. Workers と KV を直接接続し、従来の Front Line 層をバイパス
  2. 内部データ転送パスを簡素化

この変更は、KV に依存する他の Cloudflare サービス(Turnstile、Waiting Room など)にもカスケード効果をもたらしました。

整合性モデル:結果整合性のコスト

KV は結果整合性(Eventually Consistent)のストレージです。どういうことでしょうか?

データを書き込んでも、すぐにすべてのエッジノードに反映されるわけではありません。伝播には時間がかかります。公式には正確な数字はありませんが、実際のテストでは、リージョン間の伝播は通常数秒から数十秒です。

この特性は、あるシナリオでは問題になり、別のシナリオでは全く問題になりません:

問題になるシナリオ

  • ユーザーがログインしたばかりで、セッションが KV に書き込まれたが、別のリクエストが別のエッジノードにヒットしてセッションを読み取れない。ログイン「失敗」
  • リアルタイム協力編集で、ユーザーAが変更し、ユーザーBがすぐに読み取るが、最新の内容が見えない

問題にならないシナリオ

  • Feature flags 設定。変更後、数秒待ってから反映されれば完全にOK
  • API cache。サードパーティ API の戻り値を数分間キャッシュするなら、伝播遅延は問題にならない
  • Redirect mapping。URL ルールの更新が数秒遅れても、ユーザーはほとんど気付かない

即時整合性が必要なシナリオなら、KV は適さないかもしれません。この場合、Durable Objects がより良い選択肢です。特定のノードに配置され、状態の整合性を保証します。

Wrangler CLI 実践設定

理論は終わりました。実践に入りましょう。

KV の設定は2つの部分に分かれます。ネームスペース(Namespace)の作成と、wrangler.toml での Worker へのバインドです。

Namespace の作成

Namespace は KV の「コンテナ」です。各ネームスペースには無数の key-value ペアを保存できますが、アカウント全体で最大1000個のネームスペースしか持てません(この制限は2025年初頭に200から1000に引き上げられました)。

# production namespace の作成
wrangler kv namespace create MY_KV

# 出力はこのようになります:
# Created namespace with id "abc123def456..."
# Add the following to your wrangler.toml:
# [[kv_namespaces]]
# binding = "MY_KV"
# id = "abc123def456..."

ちなみに、ローカル開発テスト用の preview namespace も必要です:

# preview namespace の作成
wrangler kv namespace create MY_KV --preview

# 出力はこのようになります:
# Created preview namespace with id "preview_abc123..."

wrangler.toml 設定詳細

上記の出力の id を wrangler.toml に記入します:

name = "my-worker"
main = "src/index.ts"

[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456..."        # production namespace
preview_id = "preview_abc123..." # preview namespace(ローカル開発用)

binding という名前は重要です。Worker コード内で KV にアクセスする方法が決まります:

// binding = "MY_KV"、コード内では env.MY_KV
const value = await env.MY_KV.get("some-key");

REST API vs Workers Binding API

KV データにアクセスする方法は2つあります:

Workers Binding API(推奨):

  • Worker 内で直接 env.MY_KV.get() を使用
  • 追加のネットワークリクエストが不要で、最速
  • 完全に無料(Worker 実行時間のみカウント)

REST API

  • HTTP リクエストで KV にアクセス
  • 認証トークンが必要。外部システムからの呼び出しに適している
  • Cloudflare REST API の全体的なレート制限の対象

正直、ほとんどのシナリオで Binding API を使用すべきです。REST API は主に以下の用途に適しています:

  • 外部システムが KV データを読み書きする必要がある
  • CI/CD フローでのデータ一括インポート
  • 一時的なデバッグや運用作業

よく使う Wrangler KV コマンド

Wrangler は一連のコマンドラインツールを提供し、KV データの操作を容易にします:

# データの書き込み
wrangler kv key put --namespace-id=abc123 "my-key" "my-value"

# データの読み取り
wrangler kv key get --namespace-id=abc123 "my-key"

# データの削除
wrangler kv key delete --namespace-id=abc123 "my-key"

# 全キーのリスト(プレフィックスフィルタリング対応)
wrangler kv key list --namespace-id=abc123 --prefix="session:"

これらのコマンドはデバッグ時に便利ですが、本番環境では Worker コードで操作する方が効率的です。

TypeScript コード実践

ついにコードの部分です。完全で実行可能なサンプルを提供します。

基本的な CRUD 操作

まずは最も基本的な作成・読み取り・更新・削除です:

// src/index.ts
interface Env {
  MY_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const path = url.pathname;

    // データの書き込み
    if (path === "/put") {
      const key = url.searchParams.get("key") || "default";
      const value = url.searchParams.get("value") || "hello";
      
      await env.MY_KV.put(key, value);
      return new Response(`Saved: ${key} = ${value}`);
    }

    // データの読み取り
    if (path === "/get") {
      const key = url.searchParams.get("key") || "default";
      const value = await env.MY_KV.get(key);
      
      if (value === null) {
        return new Response("Key not found", { status: 404 });
      }
      return new Response(value);
    }

    // データの削除
    if (path === "/delete") {
      const key = url.searchParams.get("key") || "default";
      await env.MY_KV.delete(key);
      return new Response(`Deleted: ${key}`);
    }

    // キーのリスト(プレフィックス付き)
    if (path === "/list") {
      const prefix = url.searchParams.get("prefix") || "";
      const keys = await env.MY_KV.list({ prefix });
      
      const keyList = keys.keys.map(k => k.name).join("\n");
      return new Response(keyList || "No keys found");
    }

    return new Response("Try /put, /get, /delete, or /list");
  },
};

このコードは Wrangler で直接実行できます:

wrangler dev
# 書き込みテスト
curl "http://localhost:8787/put?key=test&value=helloworld"
# 読み取りテスト
curl "http://localhost:8787/get?key=test"

Session Storage 完全実装

これは KV の最も一般的なユースケースの一つです。以下に完全なセッション管理コードを示します:

// src/session.ts
interface SessionData {
  userId: string;
  email: string;
  createdAt: number;
  expiresAt: number;
}

interface Env {
  SESSION_KV: KVNamespace;
}

const SESSION_TTL = 3600; // 1時間で期限切れ

class SessionManager {
  private kv: KVNamespace;

  constructor(kv: KVNamespace) {
    this.kv = kv;
  }

  // セッションの作成
  async create(userId: string, email: string): Promise<string> {
    const sessionId = crypto.randomUUID();
    const sessionData: SessionData = {
      userId,
      email,
      createdAt: Date.now(),
      expiresAt: Date.now() + SESSION_TTL * 1000,
    };

    // KV に書き込み、TTL(自動期限切れ)を設定
    await this.kv.put(
      `session:${sessionId}`,
      JSON.stringify(sessionData),
      { expirationTtl: SESSION_TTL }
    );

    return sessionId;
  }

  // セッションの読み取り
  async get(sessionId: string): Promise<SessionData | null> {
    const raw = await this.kv.get(`session:${sessionId}`);
    if (!raw) return null;

    try {
      return JSON.parse(raw) as SessionData;
    } catch {
      return null;
    }
  }

  // セッションの削除(ログアウト)
  async delete(sessionId: string): Promise<void> {
    await this.kv.delete(`session:${sessionId}`);
  }

  // セッションの更新(有効期限の延長)
  async refresh(sessionId: string): Promise<boolean> {
    const session = await this.get(sessionId);
    if (!session) return false;

    session.expiresAt = Date.now() + SESSION_TTL * 1000;
    await this.kv.put(
      `session:${sessionId}`,
      JSON.stringify(session),
      { expirationTtl: SESSION_TTL }
    );

    return true;
  }
}

// Worker エントリーポイント
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sessionManager = new SessionManager(env.SESSION_KV);
    const url = new URL(request.url);

    // ログイン(セッション作成)
    if (url.pathname === "/login" && request.method === "POST") {
      const body = await request.json();
      const sessionId = await sessionManager.create(
        body.userId as string,
        body.email as string
      );
      
      return new Response(JSON.stringify({ sessionId }), {
        headers: { "Content-Type": "application/json" },
      });
    }

    // セッションの検証
    if (url.pathname === "/verify") {
      const sessionId = url.searchParams.get("sessionId");
      if (!sessionId) {
        return new Response("Missing sessionId", { status: 400 });
      }

      const session = await sessionManager.get(sessionId);
      if (!session) {
        return new Response("Session not found", { status: 401 });
      }

      return new Response(JSON.stringify(session), {
        headers: { "Content-Type": "application/json" },
      });
    }

    // ログアウト
    if (url.pathname === "/logout") {
      const sessionId = url.searchParams.get("sessionId");
      if (sessionId) {
        await sessionManager.delete(sessionId);
      }
      return new Response("Logged out");
    }

    return new Response("Not found", { status: 404 });
  },
};

重要なポイント:

  1. TTL 自動期限切れexpirationTtl パラメータで KV が期限切れデータを自動削除。手動クリーンアップ不要
  2. キーのプレフィックスsession: をプレフィックスとして使用し、一括クエリや異なるタイプのデータの区別を容易に
  3. JSON シリアライゼーション:KV は文字列のみ保存。複雑なオブジェクトは手動で JSON.stringify/parse が必要

API Response Cache 実装

もう一つの一般的なシナリオ:サードパーティ API の戻り値をキャッシュし、呼び出し回数とレイテンシーを削減します。

// src/api-cache.ts
interface Env {
  CACHE_KV: KVNamespace;
}

const DEFAULT_CACHE_TTL = 300; // 5分間キャッシュ

async function cachedFetch(
  kv: KVNamespace,
  cacheKey: string,
  url: string,
  ttl: number = DEFAULT_CACHE_TTL
): Promise<Response> {
  // まずキャッシュから読み取りを試みる
  const cached = await kv.get(cacheKey, "text");
  
  if (cached) {
    console.log(`Cache hit: ${cacheKey}`);
    return new Response(cached, {
      headers: {
        "Content-Type": "application/json",
        "X-Cache": "HIT",
      },
    });
  }

  // キャッシュミス、実際の API を呼び出し
  console.log(`Cache miss: ${cacheKey}`);
  const response = await fetch(url);
  const body = await response.text();

  // キャッシュに書き込み(cacheTtl で読み取りパフォーマンスを最適化)
  await kv.put(cacheKey, body, {
    expirationTtl: ttl,
    // cacheTtl: エッジキャッシュをより長く保持し、オリジンへのアクセスを削減
  });

  return new Response(body, {
    headers: {
      "Content-Type": "application/json",
      "X-Cache": "MISS",
    },
  });
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const apiUrl = url.searchParams.get("api");

    if (!apiUrl) {
      return new Response("Missing api parameter", { status: 400 });
    }

    // API URL をキャッシュキーとして使用
    const cacheKey = `api:${apiUrl}`;
    
    return cachedFetch(env.CACHE_KV, cacheKey, apiUrl);
  },
};

cacheTtl パラメータ最適化

これは KV パフォーマンス最適化で最も見落とされがちなパラメータです。

cacheTtl はエッジキャッシュの生存時間を制御します。デフォルト値は60秒で、60秒以内に同じキーを繰り返し読み取ると、エッジキャッシュから直接取得でき、オリジンに戻る必要がありません。

ホットデータの場合、cacheTtl をより高く設定できます:

// 高頻度アクセスの設定データ、より長いエッジキャッシュを設定
await env.MY_KV.get("config:feature-flags", {
  cacheTtl: 3600, // 1時間エッジキャッシュ
});

これにより、KV のセントラルストアのデータが変わらなくても、エッジノードは1時間キャッシュします。Feature flags のような「変更されても急いで反映する必要がない」シナリオで非常に便利です。

KV vs D1 vs R2 — ストレージ選択決定ガイド

正直、最初はこれに悩みました。Cloudflare がこれほど多くのストレージオプションを提供している中で、どれを選ぶべきでしょうか?

決定木を提供します:

シナリオマッチング決定木

あなたのデータはどのタイプ?

├─ ファイルストレージが必要(画像、動画、PDF)?
│   └─ YES → R2

├─ SQL クエリが必要(ユーザーテーブル、注文、複数テーブル結合)?
│   └─ YES → D1

├─ シンプルな key-value、読み取り多め・書き込み少なめ?
│   ├─ 書き込み頻度 > 1 RPS/key?
│   │   └─ YES → KV は不適切、D1 または Durable Objects を検討
│   │
│   └─ NO → KV ✓

├─ 即時整合性が必要?
│   └─ YES → Durable Objects
│   └─ NO → KV で OK の可能性

└─ わからない?
    └─ まず KV を試して、十分なら変えない
500µs-10ms
KV ホットキーレイテンシー
50-200ms
D1 レイテンシー
25MB
KV 最大 Value
数据来源: Cloudflare 公式ドキュメント

詳細比較表

比較次元KVD1R2
データモデルKey-ValueSQL(SQLite)Object Storage
クエリ能力get/put/delete のみ完全な SQL クエリクエリなし、パスのみ
書き込み制限1 RPS per keyハード制限なしハード制限なし
読み取りレイテンシー500µs - 10ms(ホット)50-200ms(場所による)高速(ダウンロード)
整合性結果整合性強整合性(単一リージョン)結果整合性
最大 value25 MBSQLite 行制限5 TB 単一ファイル
無料枠10万 reads/day5 GB ストレージ + 2500万 rows read10 GB ストレージ
典型的シナリオSession、Cache、Configユーザーデータ、注文、レポートファイル、画像、バックアップ

具体的シナリオ推奨

ユーザー認証 / Session
KV

理由:Session データはシンプルな key-value。読み取り頻度が高く(各リクエストで検証)、書き込み頻度が低い(ログイン/ログアウト時のみ)。OpenAuth などの認証フレームワークもデフォルトで KV を使用。

// session:userId → session data
await env.SESSION_KV.put(`session:${sessionId}`, JSON.stringify(session));

ユーザープロフィール / 注文管理
D1

理由:SQL クエリが必要(「特定ユーザーの全注文を検索」「先月の売上を集計」)。KV の get/put モデルではこの種のクエリができない。

-- D1 で複雑なクエリが可能
SELECT * FROM orders WHERE user_id = ? AND created_at > ?

画像 / ファイルストレージ
R2

理由:ファイルが大きすぎる(25 MB は KV の上限)。key-value の高速読み取りモードも不要。R2 はオブジェクトストレージシナリオに適している。

// R2 でファイル保存
await env.MY_BUCKET.put("images/profile.jpg", imageBuffer);

API Rate Limiting
KV(ただし注意が必要)

理由:カウンターは key-value だが、書き込み頻度が 1 RPS を超える可能性がある。単純な「本日のリクエスト回数をチェック」なら KV でも使える。正確な per-second レート制限なら、Durable Objects または Upstash Redis が必要かもしれない。

// シンプルなレート制限(毎日リセット)
const count = parseInt(await env.KV.get(`rate:${userId}`) || "0");
if (count > 100) {
  return new Response("Rate limit exceeded", { status: 429 });
}
await env.KV.put(`rate:${userId}`, String(count + 1));

サードパーティ API キャッシュ
KV

理由:API 戻り値のキャッシュ。読み取り多め、書き込み少なめ(API 呼び出し失敗または期限切れ時のみ更新)。5分間 TTL のキャッシュで即時整合性は不要。

組み合わせ使用の例

多くの場合、1つのプロジェクトで複数のストレージを組み合わせて使用します:

interface Env {
  SESSION_KV: KVNamespace;   // ユーザーセッション
  CACHE_KV: KVNamespace;     // API キャッシュ
  DATABASE_D1: D1Database;   // ユーザーデータ、注文
  FILES_R2: R2Bucket;        // ユーザーアップロードファイル
}

// 1つのリクエストで全てを使用する可能性:
// 1. SESSION_KV からセッションを読み取り
// 2. CACHE_KV からサードパーティ API キャッシュを読み取り
// 3. DATABASE_D1 でユーザー注文をクエリ
// 4. FILES_R2 からユーザーアバターを返す

このような組み合わせ使用こそが、Cloudflare ファミリーの真の威力です。

パフォーマンス最適化実践テクニック

KV を正しく使えば快適ですが、間違えればボトルネックになる可能性もあります。実際に効果があった最適化テクニックをいくつか紹介します。

1. cacheTtl パラメータチューニング

デフォルトの cacheTtl は60秒です。ホットデータの場合、この値を大幅に引き上げられます。

// ❌ デフォルト動作:60秒エッジキャッシュ
await env.KV.get("config:feature-flags");

// ✅ 最適化:設定データの場合、より長くキャッシュ
await env.KV.get("config:feature-flags", {
  cacheTtl: 3600, // 1時間エッジキャッシュ
});

どのようなシナリオで cacheTtl を増やすべきか?

  • Feature flags:設定を変更後、数秒待ってから反映されれば完全にOK
  • 静的設定:API エンドポイント、サードパーティサービス URL
  • リダイレクトルール:URL マッピングテーブル、変更頻度が低い

どのようなシナリオで適さないか?

  • Session データ:ユーザーログイン状態、即時反映が必要
  • リアルタイムカウンター:レート制限カウンター、正確さが必要

2. 並列 API 呼び出し、逐次ではなく

これは簡単に陥る落とし穴です。Worker が複数のキーを読み取る必要がある場合、一つずつ読み取らないでください。

// ❌ 逐次読み取り:各リクエストが前の完了を待つ必要がある
const user = await env.KV.get(`user:${userId}`);
const settings = await env.KV.get(`settings:${userId}`);
const permissions = await env.KV.get(`permissions:${userId}`);
// 総レイテンシー = 3 × 単一レイテンシー

// ✅ 並列読み取り:3つのリクエストが同時に発行
const [user, settings, permissions] = await Promise.all([
  env.KV.get(`user:${userId}`),
  env.KV.get(`settings:${userId}`),
  env.KV.get(`permissions:${userId}`),
]);
// 総レイテンシー ≒ 単一レイテンシー(最も遅いもの)

KV の API 呼び出しは非同期で、Worker の実行をブロックしません。Promise.all を使うことで、複数のリクエストのレイテンシーを「最も遅いもの」に圧縮できます。

実測データ:3つのキーを読み取る場合、逐次で約20ms、並列でわずか8msです。

60%
レイテンシー削減(並列 vs 逐次)
来源: 実測データ

3. ホットキー設計戦略

KV のパフォーマンスは、キーが「ホット」かどうかに大きく依存します。頻繁にアクセスされるかどうかです。

コールドキーを避ける戦略

// ❌ キーが分散しすぎ、各ユーザーが自分のキーのみアクセス
await env.KV.get(`session:${userId}`); // このユーザーのみアクセス、コールドキー

// ✅ ホットキーに統合(共有データに適用)
await env.KV.get("config:global-flags"); // 全ユーザーが共有、ホットキー

ただし、すべてのデータを1つのキーに詰め込むべきではありません。正しいアプローチは:

  • ユーザープライベートデータ:ユーザー ID でキーを分割(session、profile)
  • グローバル共有データ:単一のホットキーを使用(設定、flags、リダイレクトルール)

4. Namespace 組織のベストプラクティス

アカウントは1000個のネームスペースを持てます。この制限を活用して、異なるタイプのデータを分離できます。

# wrangler.toml
[[kv_namespaces]]
binding = "SESSION_KV"
id = "xxx"  # ユーザーセッション

[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyy"  # API キャッシュ

[[kv_namespaces]]
binding = "CONFIG_KV"
id = "zzz"  # 設定データ

メリット:

  1. 分離クリーンアップ:CACHE_KV を一括クリーンアップしても、SESSION_KV に影響しない
  2. 異なる TTL 戦略:SESSION は短い TTL、CONFIG は長い TTL
  3. モニタリング分離:Cloudflare ダッシュボードで各ネームスペースの使用量を個別に確認可能

5. バッチ操作テクニック

KV は list() 操作をサポートし、プレフィックスですべてのキーをクエリできます:

// すべてのセッションキーをリスト
const result = await env.SESSION_KV.list({ prefix: "session:" });

// result.keys は配列
for (const key of result.keys) {
  console.log(key.name);
}

// キーが多い場合、カーソルページネーションがある
if (!result.list_complete) {
  const next = await env.SESSION_KV.list({
    prefix: "session:",
    cursor: result.cursor,
  });
}

期限切れセッションの一括クリーンアップ:

// すべてのセッションをクリーンアップ(慎重に使用)
const keys = await env.SESSION_KV.list({ prefix: "session:" });
for (const key of keys.keys) {
  await env.SESSION_KV.delete(key.name);
}

注意:バッチ削除操作は慎重に。大量の書き込み枠を消費します。

価格と制限 — コスト管理ガイド

KV の価格設定は非常に友好的ですが、いくつかの制限に特别注意が必要です。落とし穴にはまると、直接エラーになる可能性があります。

100,000
無料読み取り/日
1,000
無料書き込み/日
1 GB
無料ストレージ
$5
有料プラン月額
数据来源: Cloudflare 料金ページ

Free Plan vs Paid Plan

指標Free PlanPaid Plan($5/月)
読み取り回数100,000 / 日無制限(従量課金)
書き込み回数1,000 / 日焦制限(従量課金)
削除回数1,000 / 日無制限(従量課金)
リスト回数1,000 / 日無制限(従量課金)
ストレージ容量1 GB無制限(従量課金)
Namespace 数10001000

Free plan は個人プロジェクトやテストには十分です。本番環境では Paid plan をお勧めします。月額 $5 で以下が得られます:

  • 読み取り制限なし(従量課金のみ)
  • より多くの書き込み枠
  • ダッシュボードモニタリングとアラート

書き込みレート制限:最も重要な制限

これは KV で最も重要な制限であり、最も陥りやすい落とし穴です:

各ユニークキー、毎秒最大1回の書き込み(1 RPS)

この制限を超えると、リクエストは直接エラーになります。

// ❌ 高頻度書き込みは失敗
for (let i = 0; i < 10; i++) {
  await env.KV.put("counter", String(i)); // 2回目以降は失敗
}

// ✅ キーを分散して制限を回避
await env.KV.put(`counter:${Math.floor(Date.now() / 1000)}`, value);
// 毎秒新しいキー、制限をトリガーしない

この制限の本質は何でしょうか?KV のアーキテクチャは「一度書き込み、世界中にレプリケート」です。同じキーに頻繁に書き込むと、レプリケーションのオーバーヘッドが爆発します。そのため、Cloudflare はこの制限でシステムを保護しています。

対策

  1. 時間でキーを分散counter:timestamp で毎秒新しいキー
  2. UUID で分散:毎回書き込みで新しい UUID をキーとして使用
  3. D1 または Durable Objects に変更:高頻度書き込みが必要な場合

Value サイズ制限

KV の value は最大 25 MB です(2025年初頭に10 MB から引き上げられました)。

// ❌ 25 MB を超えるとエラー
const largeData = generateBigString(30_000_000); // 30 MB
await env.KV.put("large-key", largeData); // Error!

// ✅ 大きなデータは R2 を使用
await env.R2_BUCKET.put("large-key", largeData);

25 MB は session、設定、キャッシュには十分すぎます。しかし、大きな JSON やファイルを保存する場合、R2 がより良い選択です。

Namespace 管理戦略

アカウント全体で1000個のネームスペース。2025年初頭に200からこの数字に引き上げられ、Cloudflare が制限を緩和していることを示しています。

管理戦略

// 機能別にグループ化
SESSION_KV    // ユーザーセッション
CACHE_KV      // API キャッシュ
CONFIG_KV     // 設定データ
RATE_LIMIT_KV // レート制限カウンター

ネームスペースを使い果たしたらどうすればいい?キーのプレフィックスで同じネームスペース内を分離できます:

// 単一ネームスペース内で分離
await env.KV.put("session:user1", data);
await env.KV.put("cache:api1", data);
await env.KV.put("config:flags", data);

コスト見積もり式

有料プランを使用する場合:

月額コスト = $5(基本料金)+ 読み取り費用 + 書き込み費用 + ストレージ費用

読み取り費用 = 読み取り回数 × $0.01 / 100,000
書き込み費用 = 書き込み回数 × $1.00 / 1,000,000
ストレージ費用 = ストレージサイズ × $0.50 / GB

例:毎日10万リクエストのプロジェクト:

  • 読み取り:100,000 × 30 = 3M reads/month = $0.30
  • 書き取り:仮定 1000 × 30 = 30k writes/month ≈ $0.03
  • ストレージ:10 MB × $0.50/GB ≈ $0.005
  • 総コスト:$5 + $0.33 ≈ $5.35/月

非常に安いですね。これが Cloudflare の典型的な価格設定スタイルです。

まとめ

ここまで説明してきましたが、核心は一言です。KV は Workers の「携帯メモリ」であり、session、cache、設定のような読み取り多め・書き込み少なめのシナリオに適しています。

クイック決定チェックリストを提供します:

KV を使う場合

  • データはシンプルな key-value
  • 読み取り頻度が書き込みよりはるかに高い
  • 即時整合性が不要
  • 各キーの毎秒書き込みが1回を超えない

D1 に変える場合

  • SQL クエリが必要
  • 複雑なテーブル結合がある
  • 書き込み頻度が 1 RPS を超える可能性

R2 に変える場合

  • ファイル、画像、動画を保存
  • value が 25 MB を超える

Durable Objects に変える場合

  • 即時整合性が必要
  • 協力編集、リアルタイム同期

次のステップ?Workers プロジェクトに KV を組み込んでみてください。まずは session storage から始めましょう。上記のコードは直接実行できます。問題が発生したら、Cloudflare 公式ドキュメントが詳しく書かれています。または、このシリーズの他の記事を検索してください。

D1 や R2 にも興味がある場合は、cloudflare-bindui シリーズの他の記事をチェックしてください。完全なストレージマトリックスを解説しています。

FAQ

Cloudflare Workers KV の書き込み制限は何ですか?
各ユニークキーは毎秒最大1回の書き込み(1 RPS)までです。この制限を超えると、リクエストは直接エラーになります。対策:タイムスタンプでキーを分散(counter:timestamp など)、または D1/Durable Objects に変更。
KV はユーザーセッションの保存に適していますか?
非常に適しています。Session データはシンプルな key-value で、読み取り頻度が高く(各リクエストで検証)、書き込み頻度が低い(ログイン/ログアウト時のみ)。TTL で自動期限切れを設定でき、手動クリーンアップが不要です。
KV と D1 の違いは何ですか?どちらを選ぶべき?
核心的な違い:

• KV:Key-Value モデル、ホットキーレイテンシー 500µs-10ms、書き込み制限 1 RPS/key
• D1:SQL モデル(SQLite)、複雑なクエリ対応、書き込み制限なし

選択:SQL クエリが必要なら D1、シンプルな key-value で読み取り多めなら KV。
KV のレイテンシーはなぜ 500µs-10ms に達するのですか?
3層キャッシュアーキテクチャ:Edge Cache(エッジノード)→ Regional Cache → Central Store。約30%のリクエストがエッジキャッシュで直接ヒットし、オリジンへのアクセスが不要。2025年に Cloudflare が最適化後、速度が3倍向上。
cacheTtl パラメータの役割は何ですか?
エッジキャッシュの生存時間を制御します。デフォルトは60秒。ホットデータ(feature flags、設定など)の場合、3600秒(1時間)に設定でき、エッジノードがより長くキャッシュし、オリジンへのアクセスを削減します。
KV の value は最大どれくらい保存できますか?
25 MB です(2025年初頭に10 MB から引き上げ)。この制限を超えるとエラーになります。大きなデータ(画像、動画)は R2 Object Storage を使用してください。

9 min read · 公開日: 2026年4月22日 · 更新日: 2026年4月25日

関連記事

コメント

GitHubアカウントでログインしてコメントできます