Cloudflare ハンズオン教科書|2026-08 版
第 5 章

Workers KV — グローバル key-value ストア

読み取り最速のエッジ KV でカウンタとキャッシュを作る

この章で学ぶこと

KV の特性 — 中央ストアとエッジキャッシュ

Workers KV は、グローバルに分散した低レイテンシの key-value ストアです。データは少数の中央データストアに保存され、アクセスされた地域の Cloudflare データセンターにキャッシュされます。この「中央保存 + エッジキャッシュ」の構造が KV のすべての性質を決めています。読み取りリクエストは次の順序で処理されます。

  1. アクセス元ロケーションのローカルキャッシュを確認(ヒットすれば最速。ホットキーは典型的に 500µs〜10ms 程度で応答)
  2. ミスした場合はリージョナル層のキャッシュへ
  3. それでもミスなら中央層を経て中央ストアから取得(コールドリードは遅い)
読み取り(get)— 外側から順に探し、見つかった層より外側にキャッシュされる Worker エッジキャッシュ リージョナル層 中央ストア 確認 ミス時 ミス時 値を返しつつ、経路上の各層にキャッシュする(次回は最速 500µs〜10ms) 書き込み(put)— 中央ストアへ直接。他ロケーションのキャッシュは自動更新されない Worker 中央ストア 他ロケーションの エッジキャッシュ put() 60 秒程度で失効
図 5-1 読み取りは Worker に近い層から順に探し、書き込みは中央ストアだけを更新します。破線が「時間のかかる経路」で、他ロケーションのキャッシュが失効するまで古い値が見えることが、結果整合性の正体です。

一度読まれたキーはその地域にキャッシュされるため、同じ地域からの 2 回目以降の読み取りは高速です。つまり KV は読み取りが多く書き込みが少ないワークロードに最適化されています。

一方、書き込み(put / delete)は中央ストアに対して行われ、各地のキャッシュへ自動でプッシュされるわけではありません。ここから重要な性質が生まれます。

アトミック操作やトランザクション、強整合な読み書きが必要な場合、KV は適していません。その用途には Durable Objects(第 8 章)を使います。

namespace の作成と wrangler kv コマンド

KV のデータは namespace(キーと値の集合。1 つの key-value データベース)に格納されます。namespace は npx wrangler kv namespace create <BINDING_NAME> で作成します(実行例はハンズオン 5-1)。

Worker から namespace にアクセスするには、wrangler.jsonc の kv_namespaces にバインディングを宣言します。binding は Worker コード内で env.<名前> として参照する変数名(有効な JavaScript 識別子であること)、id は作成時に発行される namespace の ID です。

{
	"kv_namespaces": [
		{
			"binding": "MY_KV",
			"id": "<YOUR_NAMESPACE_ID>"
		}
	]
}

<YOUR_NAMESPACE_ID> は npx wrangler kv namespace create の出力に表示される ID に置き換えます。バインディング名と namespace 名は一致している必要はありません。TOML 派は wrangler.toml で [[kv_namespaces]] テーブルに同じ 2 キーを書きます。

wrangler の KV コマンドは 3 系統あります。主なサブコマンドを挙げます。

系統サブコマンド用途
kv namespacecreate / list / rename / deletenamespace 自体の管理
kv keyput / get / list / delete単一キーの読み書き。--binding か --namespace-id のどちらか一方で対象を指定
kv bulkput / get / deleteJSON ファイル経由の一括操作(REST API 経由の一括書き込みは最大 10,000 ペア)

kv key put には --ttl(有効期間・秒)、--expiration(UNIX エポック秒)、--metadata(任意の JSON)といったオプションがあり、後述の Worker API と同じ機能を CLI から使えます。なお Wrangler 3.60.0 より前の kv:namespace のようなコロン区切り構文は非推奨です。古い記事を参考にする際は読み替えてください。

Worker からの get / put / list / delete

バインディングを宣言すると、Worker コードから 4 つのメソッドで KV を操作できます。すべて Promise を返すため await します。

export default {
	async fetch(request, env, ctx): Promise<Response> {
		// 書き込み(作成・上書き)
		await env.MY_KV.put("user:1", "enabled");

		// 読み取り(存在しないキーは null)
		const value = await env.MY_KV.get("user:1");

		// キーの列挙
		const listed = await env.MY_KV.list({ prefix: "user:" });

		// 削除(存在しないキーの削除も成功扱い)
		await env.MY_KV.delete("user:1");

		return Response.json({ value, keys: listed.keys });
	},
} satisfies ExportedHandler<{ MY_KV: KVNamespace }>;

get() は第 2 引数で値の型を指定できます。"text"(既定)、"json"(パース済みオブジェクト)、"arrayBuffer"、"stream" の 4 種で、大きな値ほど stream / arrayBuffer が高速です。またキーの配列を渡すと一括読み取りになり(最大 100 キー、戻り値は Map)、Worker 1 呼び出しあたり 1,000 回までの外部サービス操作制限に対して 1 操作としてカウントされる利点があります(一括読み取りは text / json 型のみ)。

expiration(TTL)と metadata

put() の第 3 引数で、キーの自動失効と付随データを設定できます。

// 相対指定: 今から 1 時間後に自動削除(最小 60 秒)
await env.MY_KV.put("session:abc", token, { expirationTtl: 3600 });

// 絶対指定: エポック秒で失効時刻を指定(例: 2027-01-01 00:00 UTC)
await env.MY_KV.put("campaign", data, { expiration: 1798761600 });

// metadata: JSON 直列化で 1024 バイトまでの付随データ
await env.MY_KV.put("docs", "https://example.com", {
	metadata: { createdAt: "2026-08-11" },
});

// 値と metadata を同時に読む
const { value, metadata } = await env.MY_KV.getWithMetadata("docs");

失効したキーは自動削除され、以後の読み取りは存在しないキーと同じ挙動になります。失効時刻は現在から 60 秒以上先である必要があります。metadata は list() の結果にも含まれるため、「一覧表示に必要な小さいデータは metadata に持たせ、list() 後の個別 get() をなくす」という定石があります。

metadata なし — 一覧の値を get() で取り直す Worker KV list() キー名 100 件 get() ×100 キー 追加の読み取り 100 回 一括 get() にまとめても課金は同じ metadata あり — list() の戻り値に同梱 Worker KV list() キー名 + metadata 個別 get() は不要 追加の読み取り 0 回 キー数が増えても増えない
図 5-2 右で消えているのは get() の辺そのものです。一括 get() は往復を 1 回にまとめられますが課金はキー数ぶん発生するため、削るべきは往復ではなく読み取り操作の数です。metadata に載せればそれが 0 になります。

list() のページネーション

list() は 1 回につき最大 1,000 キー(既定値かつ上限)を、キーの UTF-8 バイト順で返します。結果オブジェクトの形は次のとおりです。

{
	"keys": [
		{ "name": "user:1", "expiration": 1798761600, "metadata": { "plan": "free" } }
	],
	"list_complete": false,
	"cursor": "6Ck1la0VxJ0djhidm1MdX2FyD"
}

続きの有無は keys が空かどうかではなく list_complete で判定します。削除・失効直後のキー(tombstone)を走査する回では、keys が空でも続きが存在することがあるためです。list_complete が false の間、cursor を次の list({ cursor }) に渡して繰り返します。prefix で絞り込んでいる場合は 2 ページ目以降も同じ prefix を渡し続けます。

list() は 1 回で全件返るとは限らない — 続きの有無は list_complete で判定する Worker KV namespace keys[] + cursor list_complete list({ prefix }) 最大 1,000 キー list_complete が false → cursor と同じ prefix を渡して再実行 list_complete が true 取得完了
図 5-3 ループを抜ける条件は keys の中身ではなく list_complete です。色を変えた矢印が cursor を持ち回る経路で、削除・失効直後のキーだけを走査した回は keys が空のままここを 1 周します。

ユースケースと制限・無料枠

「読み取り超高速・書き込みは伝播に時間がかかる」という特性から、向き不向きは明確です。

判定ユースケース理由・代替
向くアプリ設定・機能フラグの配布、A/B テスト設定書き込み稀・読み取り多。全世界のエッジで低レイテンシに読める
向くセッションデータ・認証トークン・API キーの保存高頻度読み取り + 更新頻度低。expirationTtl で自動失効できる
向く外部 API レスポンスや静的アセットのキャッシュCache API と異なりデータセンター間で共有され、削除まで永続する
向かない同一キーを毎秒何十回も更新するカウンタやランキング同一キー 1 write/秒の制限に衝突。Durable Objects を使う
向かないアトミック操作・トランザクション・強整合が必要な処理結果整合のため。Durable Objects の強整合ストレージを使う
向かないリレーショナルなクエリ、大容量ファイル前者は D1(第 7 章)、後者は R2(第 6 章)の領分

主要な制限値と無料枠は次のとおりです(2026-08 時点)。KV は Free プランでも使えます。

項目Free プランPaid プラン
読み取り100,000 回/日1,000 万回/月 込み、超過 $0.50/100 万回
書き込み(異なるキー)1,000 回/日100 万回/月 込み、超過 $5.00/100 万回
削除 / list各 1,000 回/日各 100 万回/月 込み、超過 $5.00/100 万回
ストレージ1 GB1 GB 込み、超過 $0.50/GB-月
同一キーへの書き込み1 回/秒1 回/秒
キー / 値 / metadata サイズ512 バイト / 25 MiB / 1,024 バイト(両プラン共通)

Free プランの日次上限は毎日 00:00 UTC(日本時間 9:00)にリセットされ、超過するとその種類の操作はエラーになります。ダッシュボードや wrangler からの操作も課金対象の操作数にカウントされる点、存在しないキーの読み取り(null が返るもの)も読み取り 1 回と数えられる点に注意してください。失効で削除されたキーは、以後ストレージ使用量の課金対象に含まれません。

ハンズオン 5-1|KV namespace を作成して CLI から読み書きする

Worker プロジェクトと KV namespace を作成し、wrangler kv コマンドで最初のキーを読み書きします。所要 10 分。Free プランで完結します。

  1. プロジェクトを作成します。質問には「Hello World example」「Worker only」「TypeScript」、デプロイは「No」で答えます。
    npm create cloudflare@latest -- kv-hands-on
    cd kv-hands-on
  2. namespace を作成します。namespace のタイトルは Worker 名とバインディング名を連結した名前になります。
    npx wrangler kv namespace create COUNTER
    
    # 成功時の出力
    🌀 Creating namespace with title "kv-hands-on-COUNTER"
    ✨ Success!
    Add the following to your configuration file in your kv_namespaces array:
    {
    	"kv_namespaces": [
    		{
    			"binding": "COUNTER",
    			"id": "<YOUR_NAMESPACE_ID>"
    		}
    	]
    }
  3. 出力の kv_namespaces ブロックを wrangler.jsonc にそのまま追記します(id は自分の環境で発行された値を使います)。
  4. リモートの namespace にキーを書き込み、読み返し、一覧します。--remote を付けない場合はローカルストレージが対象になります。
    npx wrangler kv key put --binding=COUNTER "hello" "world" --remote
    npx wrangler kv key get --binding=COUNTER "hello" --text --remote
    npx wrangler kv key list --binding=COUNTER --remote

get が world を、list が [{"name": "hello"}] を表示すれば成功です。ダッシュボードの Workers KV ページでも同じキーを確認できます。

ハンズオン 5-2|アクセスカウンタ Worker を作る

KV の読み書きを Worker に組み込み、アクセスのたびに数字が増えるカウンタを作ります。所要 15 分。

  1. src/index.ts を次の内容に置き換えます。
    export default {
    	async fetch(request, env, ctx): Promise<Response> {
    		const url = new URL(request.url);
    		if (url.pathname !== "/") {
    			return new Response("Not Found", { status: 404 });
    		}
    		const current = await env.COUNTER.get("visits");
    		const count = (current === null ? 0 : parseInt(current, 10)) + 1;
    		await env.COUNTER.put("visits", String(count));
    		return new Response(`visits: ${count}`);
    	},
    } satisfies ExportedHandler<{ COUNTER: KVNamespace }>;
  2. ローカルで動作確認します。wrangler dev は既定でローカル版 KV を使うため、本番データを汚さず試せます。
    npx wrangler dev
    # 別ターミナルで数回実行
    curl http://localhost:8787/
  3. 実行のたびに visits: 1、visits: 2 … と増えることを確認したら、npx wrangler deploy でデプロイし、workers.dev の URL でも確認します。

このカウンタは KV の教材としては最適ですが、本番の厳密な集計には不向きです。同一キーへの書き込みは 1 回/秒までで、複数拠点からの同時アクセスでは結果整合性により読み取った値が古く、カウントを取りこぼします。「読んで加算して書く」を安全に行いたい場合は Durable Objects(第 8 章)が正解です。この限界を体感することも本ハンズオンの目的です。

同じキー visits を 2 拠点が更新(左から右へ時間が進む) 拠点 A get() → 41 put("42") 中央ストア visits = 41 visits = 42 visits = 42 拠点 B get() → 41 put("42") 41 を読む 42 を書く 拠点 B のキャッシュは 41 のまま 42 で上書き 2 回加算したのに 42 のまま — 1 件ぶんが消える
図 5-4 拠点 B が読むのは A の書き込みより後です。それでも 41 が返るのは、中央ストアが 42 になっても B のロケーションのキャッシュが最大 60 秒失効しないからです。書き込み間隔を空けて 429 を避けても取りこぼしは消えません。Durable Objects はこの「読む→加算→書く」を 1 か所に直列化して防ぎます。
ハンズオン 5-3|短縮 URL の骨格を作る

コードとリダイレクト先を KV に保存し、expirationTtl と metadata を実戦投入する短縮 URL サービスの骨格を作ります。所要 20 分。

  1. リンク保存用の namespace を作成し、5-1 と同様に出力を wrangler.jsonc の kv_namespaces 配列に追記します。
    npx wrangler kv namespace create LINKS
  2. src/index.ts を次の内容に置き換えます。POST でリンクを登録し、GET で 302 リダイレクトします。
    interface Env {
    	COUNTER: KVNamespace;
    	LINKS: KVNamespace;
    }
    
    export default {
    	async fetch(request, env, ctx): Promise<Response> {
    		const url = new URL(request.url);
    
    		if (request.method === "POST" && url.pathname === "/api/links") {
    			const body = (await request.json()) as { code?: string; target?: string };
    			if (!body.code || !body.target) {
    				return new Response("code and target are required", { status: 400 });
    			}
    			await env.LINKS.put(body.code, body.target, {
    				expirationTtl: 60 * 60 * 24 * 30, // 30 日で自動失効
    				metadata: { createdAt: new Date().toISOString() },
    			});
    			return Response.json({ code: body.code, target: body.target });
    		}
    
    		const target = await env.LINKS.get(url.pathname.slice(1));
    		if (target === null) {
    			return new Response("Not Found", { status: 404 });
    		}
    		return Response.redirect(target, 302);
    	},
    } satisfies ExportedHandler<Env>;
  3. npx wrangler dev を起動し、別ターミナルからリンクを登録してリダイレクトを確認します。
    curl -X POST http://localhost:8787/api/links \
    	-H "Content-Type: application/json" \
    	-d '{"code": "docs", "target": "https://developers.cloudflare.com/kv/"}'
    
    curl -i http://localhost:8787/docs

2 つ目の curl で HTTP/1.1 302 Found と location: https://developers.cloudflare.com/kv/ が返れば成功です。登録済みリンクの一覧表示を作る場合は、list() の結果に含まれる metadata(登録日時)をそのまま使えば、キーごとの get() は不要です。

つまずきポイント

wrangler kv key put したのに Worker から読めない(またはその逆)

wrangler dev と kv key put は既定でローカル版 KV を対象にします。リモート(本番)に書くには --remote フラグが必要です。逆に wrangler dev からリモートの namespace を読みたい場合は、バインディング設定に "remote": true を追加します。「書いたはずのキーが null」の最有力容疑者は、このローカル / リモートの取り違えです。

書き込んだ値がすぐ反映されない・null が返り続ける

結果整合性による正常な挙動です。他ロケーションには最大 60 秒程度(または cacheTtl の長さ)で伝播します。「存在しない」という結果もキャッシュされるため、作成直後のキーが null のままに見えることもあります。書き込み直後の読み取りに依存する設計自体を見直してください。

KV PUT failed: 429 Too Many Requests

同一キーへの書き込みが 1 秒に 1 回を超えています。1 回の Worker 呼び出し内の複数書き込みを 1 回にまとめる、書き込み先を複数キーに分散する、指数バックオフ付きリトライを実装する、のいずれかで対処します。恒常的に高頻度更新が必要なら Durable Objects への移行を検討してください。

expirationTtl を指定するとエラーになる

失効時刻は現在から 60 秒以上先が必須です。expirationTtl に 60 未満の値を渡す、あるいは expiration に 60 秒未満の近未来や過去のエポック秒を渡すと失敗します。60 秒未満の短期キャッシュがほしい場合は、読み取り側の cacheTtl(最小 30 秒)や Cache API の利用を検討してください。

理解度チェック

Q1. KV に書き込んだ値が、他のリージョンから見えるようになるまでの整合性モデルは?

結果整合性(eventually consistent)です。書き込んだロケーションでは通常すぐ見えますが(保証なし)、他のロケーションではキャッシュの失効まで、最大 60 秒程度または読み取り時の cacheTtl の長さだけ古い値が見えることがあります。

Q2. 同一キーに対して許される書き込み頻度と、超過した場合に起きることは?

同一キーへの書き込みは 1 秒に 1 回までです(プランを問わず共通)。超過すると KV PUT failed: 429 Too Many Requests エラーが発生します。異なるキーへの書き込みであればこの制限はかかりません。

Q3. put() でキーを 30 秒後に自動失効させることはできる?

できません。失効時刻(expiration / expirationTtl とも)は現在から 60 秒以上先である必要があります。なお読み取りキャッシュの cacheTtl は別物で、こちらの最小値は 30 秒です。

Q4. list() で「まだ続きのキーがあるか」を正しく判定する方法は?

list_complete が false かどうかで判定し、cursor を次の呼び出しに渡します。削除・失効直後のキーを走査する回では keys が空配列でも続きが存在しうるため、keys の空判定では不十分です。

Q5. Free プランの KV で 1 日にできる読み取りと書き込みの回数は?(2026-08 時点)

読み取り 100,000 回/日、書き込み 1,000 回/日(削除・list も各 1,000 回/日)です。上限は毎日 00:00 UTC にリセットされ、超過するとその種類の操作はエラーになります。ストレージは 1 GB までです。

まとめと次章へ

次章は、25 MiB の値上限を超える世界 — 画像・動画・任意のファイルを格納する、エグレス無料のオブジェクトストレージ R2 を扱います。