cloudflare-kv について
Cloudflare Workers KVを使った開発向けのスキルです。キー操作、キャッシュ、ページネーション、一貫性の挙動、Wranglerコマンド、利用制限を扱います。
主な機能
- 01キーの読み書き・一覧・削除
- 02キャッシュとメタデータのパターン
- 03キー一覧のページネーション
- 04一貫性とレート制限の指針
- 05Wrangler CLIコマンド例
活用シーン
- 01頻繁に読むデータのキャッシュ
- 02設定やフィーチャーフラグの保存
- 03プレフィックスごとのキー一覧取得
- 04ローカル開発でリモート名前空間を利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill cloudflare-kvインストール後、Claude Code で/cloudflare-kvを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/best-practices.mdworkers-api.mdrules/cloudflare-kv.mdtemplates/kv-basic-operations.tskv-caching-pattern.tskv-list-pagination.tskv-metadata-pattern.ts詳細
Cloudflare Workers KV
ステータス: 本番環境で利用可能 ✅ 最終更新日: 2026-01-20 依存関係: cloudflare-worker-base(Worker のセットアップ用) 最新バージョン: [email protected]、@cloudflare/[email protected]
最近の更新(2025):
- 2025年8月: アーキテクチャを再設計(パフォーマンスが40倍向上、p99レイテンシーが<5ms、R2を使ったハイブリッドストレージ)
- 2025年4月: 一括読み取り API(1回のリクエストで最大100個のキーを取得でき、1回の操作としてカウント)
- 2025年1月: ネームスペースの上限を引き上げ(Free および Paid プランで、アカウントあたり200から1,000ネームスペースに増加)
クイックスタート(5分)
# Create namespace
npx wrangler kv namespace create MY_NAMESPACE
# Output: [[kv_namespaces]] binding = "MY_NAMESPACE" id = "<UUID>"
wrangler.jsonc:
{
"kv_namespaces": [{
"binding": "MY_NAMESPACE", // Access as env.MY_NAMESPACE
"id": "<production-uuid>",
"preview_id": "<preview-uuid>" // Optional: local dev
}]
}
基本的な使い方:
type Bindings = { MY_NAMESPACE: KVNamespace };
app.post('/set/:key', async (c) => {
await c.env.MY_NAMESPACE.put(c.req.param('key'), await c.req.text());
return c.json({ success: true });
});
app.get('/get/:key', async (c) => {
const value = await c.env.MY_NAMESPACE.get(c.req.param('key'));
return value ? c.json({ value }) : c.json({ error: 'Not found' }, 404);
});
KV API リファレンス
読み取り操作
// Get single key
const value = await env.MY_KV.get('key'); // string | null
const data = await env.MY_KV.get('key', { type: 'json' }); // object | null
const buffer = await env.MY_KV.get('key', { type: 'arrayBuffer' });
const stream = await env.MY_KV.get('key', { type: 'stream' });
// Get with cache (minimum 60s)
const value = await env.MY_KV.get('key', { cacheTtl: 300 }); // 5 min edge cache
// Bulk read (counts as 1 operation)
const values = await env.MY_KV.get(['key1', 'key2']); // Map<string, string | null>
// With metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('key');
const result = await env.MY_KV.getWithMetadata(['key1', 'key2']); // Bulk with metadata
書き込み操作
// Basic write (max 1/second per key)
await env.MY_KV.put('key', 'value');
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
// With expiration
await env.MY_KV.put('session', data, { expirationTtl: 3600 }); // 1 hour
await env.MY_KV.put('token', value, { expiration: Math.floor(Date.now()/1000) + 86400 });
// With metadata (max 1024 bytes)
await env.MY_KV.put('config', 'dark', {
metadata: { updatedAt: Date.now(), version: 2 }
});
重要な制限:
- キー: 最大512バイト
- 値: 最大25 MiB
- メタデータ: 最大1024バイト
- 書き込みレート: キーあたり1秒に1回(超過すると429エラー)
- 有効期限: 最短60秒
一覧表示操作
// List with pagination
const result = await env.MY_KV.list({ prefix: 'user:', limit: 1000, cursor });
// result: { keys: [], list_complete: boolean, cursor?: string }
// CRITICAL: Always check list_complete, not keys.length === 0
let cursor: string | undefined;
do {
const result = await env.MY_KV.list({ prefix: 'user:', cursor });
processKeys(result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
削除操作
// Delete single key
await env.MY_KV.delete('key'); // Always succeeds
// Bulk delete (CLI only, up to 10,000 keys)
// npx wrangler kv bulk delete --binding=MY_KV keys.json
高度なパターン
CacheTtl を使ったキャッシュパターン
async function getCachedData(kv: KVNamespace, key: string, fetchFn: () => Promise<any>, ttl = 300) {
const cached = await kv.get(key, { type: 'json', cacheTtl: ttl });
if (cached) return cached;
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl * 2 });
return data;
}
ガイドライン: 最短60秒に設定し、読み取り頻度の高いワークロード(読み取りと書き込みの比率が100:1)に使用します。
メタデータの最適化
// Store small values (<1024 bytes) in metadata to avoid separate get() calls
await env.MY_KV.put('user:123', '', {
metadata: { status: 'active', plan: 'pro', lastSeen: Date.now() }
});
// list() returns metadata automatically (no additional get() calls)
const users = await env.MY_KV.list({ prefix: 'user:' });
users.keys.forEach(({ name, metadata }) => console.log(name, metadata.status));
ホットキーとコールドキーについて
KV のパフォーマンスは、キーの温度によって異なります。
| 種類 | 応答時間 | 発生条件 |
|---|---|---|
| ホットキー | 6-8ms | データセンターごとに1分あたり2回以上読み取られる |
| コールドキー | 100-300ms | アクセス頻度が低く、中央ストレージから取得される |
2025年8月以降の改善点:
- すべての KV Worker 呼び出しにおける P90: <12ms(従来は22ms)
- ホットキーの読み取りが最大3倍高速化
- すべての操作が最大20ms高速化
最適化: キーの統合を使うと、コールドキーもホットキーのキャッシュの恩恵を受けられます。
// ❌ Bad: Many cold keys (300ms each)
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', '[email protected]');
await kv.put('user:123:plan', 'pro');
// Each read of a cold key: ~100-300ms
const name = await kv.get('user:123:name'); // Cold
const email = await kv.get('user:123:email'); // Cold
const plan = await kv.get('user:123:plan'); // Cold
// ✅ Good: Single hot key (6-8ms)
await kv.put('user:123', JSON.stringify({
name: 'John',
email: '[email protected]',
plan: 'pro'
}));
// Single read, cached as hot key: ~6-8ms
const user = JSON.parse(await kv.get('user:123'));
CacheTtl はコールドキーに有効です: 読み取り頻度の低いデータでは、cacheTtl によりコールド読み取りのレイテンシーが低下します。
トレードオフ: 更新時には、統合のために読み取り・変更・書き込みが必要です。
ページネーションヘルパー
async function* paginateKV(kv: KVNamespace, options: { prefix?: string } = {}) {
let cursor: string | undefined;
do {
const result = await kv.list({ ...options, cursor });
yield result.keys;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
}
// Usage
for await (const keys of paginateKV(env.MY_KV, { prefix: 'user:' })) {
processKeys(keys);
}
指数バックオフを使ったレート制限時の再試行
async function putWithRetry(kv: KVNamespace, key: string, value: string, opts?: KVPutOptions) {
let attempts = 0, delay = 1000;
while (attempts < 5) {
try {
await kv.put(key, value, opts);
return;
} catch (error) {
if ((error as Error).message.includes('429')) {
attempts++;
if (attempts >= 5) throw new Error('Max retry attempts');
await new Promise(r => setTimeout(r, delay));
delay *= 2; // Exponential backoff
} else throw error;
}
}
}
結果整合性について
KV は、Cloudflare のグローバルネットワーク全体で結果整合性を備えています(2025年8月の再設計: ハイブリッドストレージ、p99レイテンシー<5ms)。
仕組み:
- 同じロケーション内では書き込みがすぐに反映されます(同じ POP 内での Read-Your-Own-Write 整合性)
- 他のロケーションには、約60秒以内(または cacheTtl の値)に更新が反映されます
- 伝播中のキャッシュ読み取りでは、古いデータが返される場合があります
例:
// Tokyo: Write
await env.MY_KV.put('counter', '1');
const value = await env.MY_KV.get('counter'); // "1" ✅ (same POP, RYOW)
// London (within 60s): May be stale ⚠️
const value2 = await env.MY_KV.get('counter'); // Might be old value
// After 60+ seconds: Consistent ✅
Read-Your-Own-Write(RYOW)の保証: 2025年8月の再設計以降、同じ Cloudflare ポイント・オブ・プレゼンスを経由するリクエストでは、自身の書き込みがすぐに反映されます。異なる POP 間でのグローバルな整合性には、引き続き最大60秒かかります。
タイムスタンプによる軽減パターン(厳密な整合性が重要な場合):
// Use timestamp in key structure to avoid consistency issues
const timestamp = Date.now();
await kv.put(`user:123:${timestamp}`, userData);
// Find latest using list with prefix
const result = await kv.list({ prefix: 'user:123:' });
const latestKey = result.keys.sort((a, b) =>
parseInt(b.name.split(':')[2]) - parseInt(a.name.split(':')[2])
).at(0);
KV の用途: 読み取り頻度の高いワークロード(比率100:1)、設定、機能フラグ、キャッシュ、ユーザー設定 KV を使わない用途: 金融取引、強い整合性、キーあたり1秒に1回を超える書き込み、重要なデータ
強い整合性が必要ですか? Durable Objects を使用してください。
出典: Workers KV の再設計
Wrangler CLI の基本
# Create namespace
npx wrangler kv namespace create MY_NAMESPACE [--preview]
# Manage keys (add --remote flag to access production data)
npx wrangler kv key put --binding=MY_KV "key" "value" [--ttl=3600] [--metadata='{}']
npx wrangler kv key get --binding=MY_KV "key" [--remote]
npx wrangler kv key list --binding=MY_KV [--prefix="user:"] [--remote]
npx wrangler kv key delete --binding=MY_KV "key"
# Bulk operations (up to 10,000 keys)
npx wrangler kv bulk put --binding=MY_KV data.json
npx wrangler kv bulk delete --binding=MY_KV keys.json
重要: CLI コマンドはデフォルトでローカルストレージを使用します。本番環境またはリモートのデータにアクセスするには、--remote フラグを追加してください。
開発環境と本番環境
ローカル開発でのリモートバインディング(Wrangler 4.37+)
開発中にローカルの Workers を本番環境の KV 名前空間に接続します。
wrangler.jsonc:
{
"kv_namespaces": [{
"binding": "MY_KV",
"id": "production-uuid",
"remote": true // Connect to live KV
}]
}
仕組み:
- ローカルの Worker コードはローカルで実行されます(高速に反復できます)
- KV 操作はプロキシを経由して本番環境の名前空間にルーティングされます
- 手動でのデータ投入は不要です
メリット:
- デプロイせずに実際の本番データを使ってテストできます
- 本番データにアクセスしながら、ローカルでコードを高速に実行できます
- フィードバックループが速くなります(デプロイとテストを繰り返す必要がありません)
⚠️ 警告:書き込みは本番データに影響します。本番環境の代わりに、remote: true を設定したステージング名前空間の使用を検討してください。
バージョンサポート:
- Wrangler 4.37.0+
- @cloudflare/vite-plugin 1.13.0+
- @cloudflare/vitest-pool-workers 0.9.0+
制限とクォータ
| 機能 | 無料プラン | 有料プラン |
|---|---|---|
| 1 日あたりの読み取り回数 | 100,000 | 無制限 |
| 1 日あたりの書き込み回数(異なるキー) | 1,000 | 無制限 |
| キーあたりの 1 秒間の書き込み回数 | 1 | 1 |
| Worker の呼び出しあたりの操作数 | 1,000 | 1,000 |
| アカウントあたりの名前空間数 | 1,000 | 1,000 |
| アカウントあたりのストレージ容量 | 1 GB | 無制限 |
| キーのサイズ | 512 bytes | 512 bytes |
| メタデータのサイズ | 1024 bytes | 1024 bytes |
| 値のサイズ | 25 MiB | 25 MiB |
| cacheTtl の最小値 | 60 seconds | 60 seconds |
重要:キーあたりの書き込みは 1 秒間に 1 回です(超過すると 429)。一括操作は 1 操作としてカウントされます。名前空間の上限は 200 から 1,000 に引き上げられました(2025 年 1 月)。
エラー処理
1. レート制限(429 Too Many Requests)
原因:同じキーに 1 秒間に 1 回を超えて書き込んでいます
解決策:指数バックオフを使った再試行を使用します(高度なパターンを参照)
// ❌ Bad
await env.MY_KV.put('counter', '1');
await env.MY_KV.put('counter', '2'); // 429 error!
// ✅ Good
await putWithRetry(env.MY_KV, 'counter', '2');
2. 値が大きすぎる
原因:値が 25 MiB を超えています
解決策:書き込み前にサイズを検証します
if (value.length > 25 * 1024 * 1024) throw new Error('Value exceeds 25 MiB');
3. メタデータが大きすぎる
原因:シリアル化後のメタデータが 1024 bytes を超えています
解決策:シリアル化後のサイズを検証します
const serialized = JSON.stringify(metadata);
if (serialized.length > 1024) throw new Error('Metadata exceeds 1024 bytes');
4. 無効な CacheTtl
原因:cacheTtl が 60 秒未満です
解決策:最小値の 60 を使用します
// ❌ Error
await env.MY_KV.get('key', { cacheTtl: 30 });
// ✅ Correct
await env.MY_KV.get('key', { cacheTtl: 60 });
重要なルール
必ず行うこと ✅
- 複数のキーを読み取る場合は一括操作を使用します(1 操作としてカウントされます)
- 頻繁に読み取られ、更新頻度が低いデータには cacheTtl を設定します(最小 60 秒)
list()を頻繁に使用する場合は、小さな値(<1024 bytes)をメタデータに保存します- ページネーションでは
keys.length === 0ではなく、list_completeを確認します - 書き込み操作には、指数バックオフを使った再試行ロジックを使用します
- 書き込み前にサイズを検証します(キー 512B、値 25MiB、メタデータ 1KB)
- キャッシュ性能を高めるため、関連するキーをまとめます
- KV は読み取りが多いワークロードに使用します(読み取りと書き込みの比率は 100:1 が理想です)
絶対に行わないこと ❌
- 同じキーに 1 秒間に 1 回を超えて書き込まないでください(429 レート制限エラーが発生します)
- グローバルに即時一貫性が保たれると想定しないでください(伝播には約 60 秒かかります)
- アトミック操作に KV を使用しないでください(代わりに Durable Objects を使用します)
- cacheTtl を 60 秒未満に設定しないでください(失敗します)
- 名前空間 ID を公開リポジトリにコミットしないでください(環境変数を使用します)
- 呼び出しあたりの操作数を 1,000 を超えないようにしてください(一括操作を使用します)
- 書き込み順序に依存しないでください(結果整合性のため、順序は保証されません)
- null 値の処理を忘れないでください(キーが存在しない場合、
get()はnullを返します)
トラブルシューティング
問題 1:書き込み時に「429 Too Many Requests」が発生する
原因:同じキーに 1 秒間に 1 回を超えて書き込んでいます
解決策:書き込みをまとめるか、指数バックオフを使った再試行を使用します
// ❌ Bad: Rate limit
for (let i = 0; i < 10; i++) await kv.put('counter', String(i));
// ✅ Good: Single write
await kv.put('counter', '9');
// ✅ Good: Retry with backoff
await putWithRetry(kv, 'counter', String(i));
問題 2:書き込み後に古い値が読み取られる
原因:結果整合性(伝播に約 60 秒かかります)
解決策:古い値の読み取りを許容するか、強い一貫性が必要な場合は Durable Objects を使用するか、アプリケーションレベルでキャッシュ無効化を実装します
問題 3:「Operations limit exceeded」が発生する
原因:1 回の Worker 呼び出しで KV 操作を 1,000 回以上行っています
解決策:一括操作を使用します
// ❌ Bad: 5000 operations
for (const key of 5000keys) await kv.get(key);
// ✅ Good: 1 operation
const values = await kv.get(keys); // Bulk read
Issue 4: リストが空を返すが、カーソルが存在する
原因: 削除済み/期限切れのキーによって「トゥームストーン」が作成され、これらを反復処理する必要があります
解決策: keys.length ではなく、必ず list_complete を確認してください
// ✅ Correct pagination
let cursor: string | undefined;
do {
const result = await kv.list({ cursor });
processKeys(result.keys); // Even if empty
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
重要: prefix を使用する場合、ページネーションを行うすべての呼び出しに含める必要があります。
// ❌ WRONG - Loses prefix on subsequent pages
let result = await kv.list({ prefix: 'user:' });
result = await kv.list({ cursor: result.cursor }); // Missing prefix!
// ✅ CORRECT - Include prefix on every call
let cursor: string | undefined;
do {
const result = await kv.list({ prefix: 'user:', cursor });
processKeys(result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
出典: キーのリストに関するドキュメント
Issue 5: wrangler types では環境内にネストされた KV バインディングの型が生成されない
原因: 環境設定内(例: [env.feature.kv_namespaces])で定義された KV 名前空間は、生成される TypeScript の型に含まれません
影響: KV バインディングの TypeScript オートコンプリートと型チェックが失われます
出典: GitHub Issue #9709
設定例:
# wrangler.toml
[env.feature]
name = "my-worker-feature"
[[env.feature.kv_namespaces]]
binding = "MY_STORAGE_FEATURE"
id = "xxxxxxxxxxxx"
npx wrangler types を実行すると、環境変数の型定義は作成されますが、KV 名前空間のバインディングの型定義は作成されません。
回避策:
# Generate types for specific environment
npx wrangler types -e feature
または、KV 名前空間を環境内にネストせず、トップレベルに定義してください。
# Top-level (types generated correctly)
[[kv_namespaces]]
binding = "MY_STORAGE"
id = "xxxxxxxxxxxx"
注: ランタイムのバインディングは引き続き正常に動作します。この問題は型生成にのみ影響します。
Issue 6: wrangler kv key list がリモートデータに対して空の配列を返す
原因: CLI コマンドはリモート/本番環境の KV ではなく、ローカルストレージをデフォルトで使用します 影響: ユーザーは本番データが表示されることを期待しますが、ローカルストレージから空の配列が返されます 出典: GitHub Issue #10395
解決策: --remote フラグを使用して、本番/リモートデータにアクセスしてください
# ❌ Shows local storage (likely empty)
npx wrangler kv key list --binding=MY_KV
# ✅ Shows remote/production data
npx wrangler kv key list --binding=MY_KV --remote
この問題が起きる理由: 仕様上、wrangler dev は本番データへの干渉を避けるため、ローカル KV ストレージを使用します。一貫性を保つため、CLI コマンドも同じ設定をデフォルトで使用します。
対象: すべての wrangler kv key コマンド(get、list、delete、put)
本番環境向けチェックリスト
- 環境ごとの名前空間を設定済み(
idとpreview_id) - 名前空間 ID を環境変数に保存済み(ハードコードしていない)
- 書き込み時のレート制限に対する再試行ロジックを実装済み
- 読み取り用に適切な
cacheTtl値を設定済み(最小 60s) - サイズを検証済み(キー 512B、値 25MiB、メタデータ 1KB)
- 可能な場合は一括操作を使用
-
list_completeを確認するページネーションを実装済み(keys.lengthではない) - null 値のエラー処理を実装済み
- レート制限の監視/アラートを設定済み
関連ドキュメント
最終更新日: 2026-01-20
パッケージのバージョン: [email protected], @cloudflare/[email protected]
変更内容: 6 件の調査結果を追加 — ホットキー/コールドキーのパフォーマンスパターン、リモートバインディング(Wrangler 4.37+)、wrangler types の環境に関する問題、CLI の --remote フラグ要件、RYOW の一貫性に関する詳細、ページネーションにおける prefix の維持