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

Cloudflare Workers KV 実践:分散型キーバリューストアを基礎から使いこなす

Cloudflare Dashboard 上のレイテンシ曲線をじっと見つめる。あの赤い線はまだ 200ms 以上をうろついている——Workers を使っていて、コードも十分に絞り込んだはずなのに、なぜユーザーのリクエストはこんなにも待たされるのか?

問題はデータベースにあった。session クエリのたびに、エッジノードからヨーロッパのデータセンターまで往復していたのだ。この行って帰ってだけで、たとえ Workers の実行に 5ms しかかからなくても、ネットワーク転送が時間を食い尽くしてしまう。

後になってようやく分かった。Workers 自体はステートレスなのだ。本当の意味で「エッジに住む」ストレージが必要だった——それが Cloudflare Workers KV だ。

この記事では、自分がハマった落とし穴、計測したデータ、書いたコードを余すところなく並べていく。KV とはそもそも何か、なぜ sub-10ms のレイテンシを実現できるのかから、session storage と API cache の完全実装まで。さらに、いつ KV を使うべきか、いつ D1 や R2 のほうが適しているのかも語っていく。

KV とは何か — 分散型エッジストレージを理解する

ざっくり言えば、KV は Cloudflare が Workers のために用意した「携帯メモリ」のようなものだ。このメモリはどこか固定の機室に置かれているわけではなく、世界中の 300 以上のエッジノードに散らばって分布している。東京のユーザーがリクエストすれば、データは東京のエッジノードで待っているかもしれない。フランクフルトのリクエストなら、データはすでにフランクフルトのキャッシュにあるかもしれない。

Cloudflare Workers KV は、エッジコンピューティング向けに設計されたグローバル分散型キーバリューストアだ。その最も中核的な特徴は三つある。

読み取りが超高速。hot keys のキャッシュヒット時のレイテンシは 500µs から 10ms の間だ——正直、最初にこの数字を見たときは少し疑っていた。自分で一連のベンチマークを回してみるまでは。確かに、安定して一桁ミリ秒のレベルに収まっていた。

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

高スループット。単一の key の読み取りは数千 RPS(requests per second)に達することができる。データがエッジにキャッシュされているため、毎回オリジンへ戻る必要がないからだ。

500µs - 10ms
Hot Keys のレイテンシ範囲

Cloudflare ストレージ製品群の比較

KV は Cloudflare のストレージマトリクスの 1 つのピースにすぎない。まずは全体像を見てみよう。

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

この表を見ると、こう疑問に思うかもしれない。1 RPS/key とはどういう意味か? これは後で詳しく説明するが、簡単に言えば key ごとに 1 秒あたり 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 をデフォルトの session storage として採用している——これについては後で完全な実装コードを示す。

KV アーキテクチャ徹底解説 — なぜこんなに速いのか

KV の速さは魔法ではなく、三層キャッシュ構成の結果だ。

コンビニで買い物をする様子を想像してほしい。最も理想的なのは、商品がレジ脇の棚にあって手を伸ばせば取れる状態(edge cache)。少し劣ると、商品は倉庫にあって店員が取りに行く(regional cache)。最も遅いのは、商品が総倉庫にあってトラックで運んでくるのを待つ状態(central store)だ。

KV のアーキテクチャはまさにこの三層になっている。

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

Cloudflare の 2025 年 10 月のブログデータによれば、約 30% のリクエストがキャッシュ層で直接解決できる。つまり読み取りの 3 分の 1 はそもそも中央ストレージへ戻る必要がなく、レイテンシは自然と下がる。

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

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

Cloudflare の公式ドキュメントは一連の参考データを示している。

  • Hot keys(頻繁にアクセスされる key):500µs から 10ms
  • Cold keys(初回アクセスまたは稀にしかアクセスされない):レイテンシが高く、オリジンへ戻る必要がある

正直なところ、「500µs」という数字は最初あまり信じていなかった。後で自分で計測してみた。

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

100 回計測したところ、hot keys の平均レイテンシは確かに 5-8ms ほどだった。cold keys は初回アクセスで 50ms 以上になるが、2 回目のアクセスでは下がった——キャッシュが効いたのだ。

2025 年に Cloudflare は KV に大改造を施し、公式ブログのデータによれば操作速度は 3 倍に向上した。主に二つの変更があった。

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

この変更は KV に依存する他の Cloudflare サービス(Turnstile、Waiting Room など)にも連鎖的な恩恵をもたらした。

整合性モデル:結果整合性の代償

KV は結果整合性(eventually consistent)のストレージだ。どういう意味か?

データを 1 件書き込んでも、すべてのエッジノードに即座に現れるわけではない。伝播には時間がかかる——公式は正確な数字を示していないが、実際のテストでは、リージョンをまたぐ伝播は通常数秒から数十秒の間だ。

この特性は、あるシーンでは問題になり、別のシーンではまったく重要ではない。

問題になるシーン

  • ユーザーがログインしたばかりで session を KV に書き込んだが、別のリクエストが別のエッジノードに届き、session が読めない——ログイン「失敗」
  • リアルタイム共同編集で、A ユーザーが変更し、B ユーザーがすぐ読むと、最新の内容が見えない

問題にならないシーン

  • Feature flags の設定、変更後に数秒待って反映されてもまったく OK
  • API cache、サードパーティ API の返り値を数分キャッシュ、伝播遅延は気にならない
  • Redirect mapping、URL ルールの更新が数秒遅れても、ユーザーはほぼ気づかない

もしあなたのシーンが即時整合性を必要とするなら、KV は適さないかもしれない。その場合は Durable Objects のほうがよい選択になる——特定ノードへ配置され、状態の一貫性を保証してくれる。

Wrangler CLI 実践設定

理論は終わった。手を動かし始めよう。

KV の設定は二つの部分に分かれる。namespace(名前空間)を作成し、それを wrangler.toml であなたの Worker にバインドする。

Namespace の作成

Namespace は KV の「コンテナ」だ。各 namespace には無数の key-value pair を保存できるが、アカウント全体で namespace は合計 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 データにアクセスする方法は二つある。

Workers Binding API(推奨):

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

REST API

  • HTTP リクエストで KV にアクセスする
  • 認証 token が必要で、外部システムからの呼び出しに向く
  • 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"

# すべての 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}`);
    }

    // keys を列挙する(プレフィックス付き)
    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 で最もよくある応用シーンの一つだ。以下は完全な session 管理コードだ。

// 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;
  }

  // session を作成
  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;
  }

  // session を読み取り
  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;
    }
  }

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

  // session をリフレッシュ(期限を延長)
  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);

    // ログイン(session を作成)
    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" },
      });
    }

    // session を検証
    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. key プレフィックス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 をキャッシュ key にする
    const cacheKey = `api:${apiUrl}`;
    
    return cachedFetch(env.CACHE_KV, cacheKey, apiUrl);
  },
};

cacheTtl パラメータの最適化

これは KV のパフォーマンス最適化で最も見落とされやすいパラメータだ。

cacheTtl はエッジキャッシュの生存時間を制御する。デフォルト値は 60 秒で、60 秒以内に同じ key を繰り返し読み取れば、オリジンへ戻らずエッジキャッシュから直接取得できることを意味する。

ホットデータには、cacheTtl をより高く設定できる。

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

こうすれば、たとえ KV の central store のデータが変わっていなくても、エッジノードは 1 時間キャッシュする。feature flags のように「変えてもすぐ反映されなくてよい」シーンには、とても有用だ。

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

この部分は正直、自分も最初は迷った。Cloudflare はこんなにたくさんのストレージ選択肢を用意しているが、いったいどれを選べばいいのか?

意思決定ツリーを示そう。

シーンマッチング意思決定ツリー

あなたのデータはどんな種類?

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

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

├─ 単純な key-value で、読み取り多め書き込み少なめ?
│   ├─ 書き込み頻度 &gt; 1 RPS/key?
│   │   └─ YES → KV には不向き、D1 か Durable Objects を検討
│   │
│   └─ NO → KV ✓

├─ 即時整合性が必要?
│   └─ YES → Durable Objects
│   └─ NO → KV で OK かもしれない

└─ 分からない?
    └─ まず KV を試して、足りていれば変えない
500µs-10ms
KV Hot Key レイテンシ
50-200ms
D1 レイテンシ
25MB
KV 最大 Value
Source: Cloudflare 公式ドキュメント

詳細比較表

比較項目KVD1R2
データモデルKey-ValueSQL(SQLite)Object Storage
クエリ能力get/put/delete のみ完全な SQL クエリクエリなし、パスのみ
書き込み制限1 RPS per keyハード制限なしハード制限なし
読み取りレイテンシ500µs - 10ms(hot)50-200ms(位置に依存)速い(ダウンロード)
整合性結果整合強整合(単一リージョン)結果整合
最大 value25 MBSQLite の行制限単一ファイル 5 TB
無料枠100k reads/day5 GB ストレージ + 25M 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 &gt; ?

画像 / ファイルストレージ
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 rate limiting なら Durable Objects か Upstash Redis が必要かもしれない。

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

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

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

組み合わせて使う例

多くの場合、一つのプロジェクトでは複数のストレージを組み合わせて使う。

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

// 1 つのリクエストですべてを使うこともある:
// 1. SESSION_KV から session を読み取る
// 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 endpoint、サードパーティサービスの URL
  • リダイレクトルール:URL マッピング表、変化頻度が低い

向いていないシーンは?

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

2. API 呼び出しは直列ではなく並列で

これはハマりやすい落とし穴だ。Worker が複数の key を読み取る必要があるなら、一つずつ順番に読まないこと。

// ❌ 直列読み取り:各リクエストが前のものの完了を待つ
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 つの key を読むと、直列で約 20ms、並列ではわずか 8ms。

60%
レイテンシ削減(並列 vs 直列)
Source: 実測データ

3. Hot Keys 設計戦略

KV のパフォーマンスは、あなたの key が「hot」かどうか——頻繁にアクセスされるかどうかに大きく左右される。

cold keys を避ける戦略

// ❌ key が分散しすぎ、各ユーザーが自分の key にしかアクセスしない
await env.KV.get(`session:${userId}`); // このユーザーしかアクセスしない、cold key

// ✅ hot key にまとめる(共有データに適用)
await env.KV.get("config:global-flags"); // 全ユーザーが共有、hot key

ただし、これはすべてのデータを 1 つの key に詰め込めという意味ではない。正しいやり方はこうだ。

  • ユーザー個別データ:ユーザー ID で key を分ける(session、profile)
  • グローバル共有データ:単一の hot key を使う(設定、flags、リダイレクトルール)

4. Namespace 編成のベストプラクティス

アカウントは 1000 個の namespace を持てる。この上限をうまく使えば、異なる種類のデータを分離できる。

# wrangler.toml
[[kv_namespaces]]
binding = "SESSION_KV"
id = "xxx"  # ユーザー session

[[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 Dashboard で namespace ごとの使用量を分けて見られる

5. 一括操作のテクニック

KV は list() 操作をサポートしており、プレフィックスですべての key をクエリできる。

// すべての session keys を列挙
const result = await env.SESSION_KV.list({ prefix: "session:" });

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

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

期限切れ session の一括クリーンアップ:

// すべての session をクリーンアップ(慎重に使うこと)
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
Paid Plan 月額
Source: Cloudflare 料金ページ

Free Plan vs Paid Plan

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

Free plan は個人プロジェクトやテストには十分だ。本番環境では Paid plan をおすすめする——月 $5 で、次のものが手に入る。

  • 読み取り無制限(従量課金のみ)
  • より多くの書き込み枠
  • Dashboard のモニタリングとアラート

Write Rate Limit:最も重要な制限

これは KV で最も重要な制限であり、最もハマりやすいポイントでもある。

unique key ごとに、1 秒あたり最大 1 回(1 RPS)まで書き込み可能

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

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

// ✅ key を分散させれば制限を回避できる
await env.KV.put(`counter:${Math.floor(Date.now() / 1000)}`, value);
// 毎秒新しい key で、制限に引っかからない

この制限の本質は何か? KV のアーキテクチャは「一度書いて、世界中に複製する」というものだ。同じ key に頻繁に書き込むと、複製のオーバーヘッドが爆発する。だから Cloudflare はこの制限で自分のシステムを保護している。

対策

  1. 時間で key を分散counter:timestamp で毎秒新しい key
  2. UUID で分散:書き込みのたびに新しい UUID を key にする
  3. D1 か Durable Objects に切り替える:高頻度書き込みが必須なら

Value Size 制限

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 管理戦略

アカウント全体で namespace は 1000 個。2025 年初頭に 200 からこの数字へ引き上げられたことは、Cloudflare が制限を緩めていることを示している。

管理戦略

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

namespace を使い切ったらどうするか? key プレフィックスを使えば、同じ namespace 内で分離できる。

// 単一 namespace 内での分離
await env.KV.put("session:user1", data);
await env.KV.put("cache:api1", data);
await env.KV.put("config:flags", data);

コスト試算の公式

プロジェクトで Paid plan を使う場合:

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

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

例:1 日 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/month

かなり安い、そうだろう? これは Cloudflare の典型的な料金スタイルだ。

まとめ

ここまで語ってきたが、核心は一言だ。KV は Workers の「携帯メモリ」であり、session、cache、設定といった読み取り多め書き込み少なめのシーンに向いている。

クイック意思決定リストを示そう。

KV を使う、もし

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

D1 に切り替える、もし

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

R2 に切り替える、もし

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

Durable Objects に切り替える、もし

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

次のステップは? あなたの Workers プロジェクトに KV を組み込んでみよう。まずは session storage から始めるとよい——上のコードはそのまま動かせる。問題に遭遇したら、Cloudflare の公式ドキュメントはかなり詳しく書かれているし、このシリーズの他の記事を直接検索してもいい。

D1 や R2 にも興味があれば、cloudflare-bindui シリーズの他の記事を見てほしい——そこで完全なストレージマトリクスを余すところなく説明していく。

FAQ

Cloudflare Workers KV の書き込み制限とは何ですか?
unique key ごとに 1 秒あたり最大 1 回(1 RPS)までです。この制限を超えると、リクエストはそのままエラーになります。対策:タイムスタンプで key を分散させる(例:counter:timestamp)か、D1 / Durable Objects に切り替えましょう。
KV はユーザー session の保存に向いていますか?
非常に向いています。session データは単純な key-value で、読み取り頻度が高く(リクエストごとに検証)、書き込み頻度は低い(ログイン/ログアウト時のみ)。TTL による自動期限切れと組み合わせれば、手動でのクリーンアップは不要です。
KV と D1 は何が違いますか?どちらを選べばよいですか?
中核的な違い:

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

選び方:SQL クエリが必要なら D1、単純な key-value で読み取り多め書き込み少なめなら KV。
KV のレイテンシはなぜ 500µs〜10ms を実現できるのですか?
三層キャッシュ構成によるものです: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 を使いましょう。

8分で読めます · 公開日: 2026年4月22日 · 更新日: 2026年6月8日

関連記事

コメント

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