mcpField
C
cloudflare-kv

cloudflare-kv

github.com/jackspaceデータ・データベース1640 回インストール12 週間前に更新
リポジトリ

cloudflare-kv について

cloudflare-kv は、Cloudflare Workers KV の設定や運用について、TypeScript と Wrangler の手順をまとめたスキルです。グローバルな結果整合性を許容できる、読み取り中心の Worker アプリケーション向けです。

主な機能

  1. 01ネームスペースの作成とバインディング設定
  2. 02読み取り、書き込み、一覧表示、削除
  3. 03一括操作とページネーションのパターン
  4. 04キャッシュ、メタデータ、キー統合の実践
  5. 05整合性の特性、制限、一般的なエラー

活用シーン

  1. 01Worker で設定やユーザー設定を保存
  2. 02KV をキャッシュや機能フラグに利用
  3. 03Wrangler によるキーと値の管理
  4. 04読み取り中心のアプリ向け KV ストレージ設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-kv

インストール後、Claude Code で/cloudflare-kvを入力して読み込まれたか確認します。

同梱ファイルREADME.mdSKILL.json

詳細

Cloudflare Workers KV

ステータス:本番環境で使用可能 ✅ 最終更新日:2025-10-21 依存関係:cloudflare-worker-base(Worker のセットアップ用) 最新バージョン:[email protected]、@cloudflare/[email protected]


クイックスタート(5 分)

1. KV 名前空間を作成する
# Create a new KV namespace
npx wrangler kv namespace create MY_NAMESPACE

# Output includes namespace_id - save this!
# ✅ Success!
# Add the following to your wrangler.toml or wrangler.jsonc:
#
# [[kv_namespaces]]
# binding = "MY_NAMESPACE"
# id = "<UUID>"

開発用(プレビュー)名前空間の場合:

npx wrangler kv namespace create MY_NAMESPACE --preview

# Output:
# [[kv_namespaces]]
# binding = "MY_NAMESPACE"
# preview_id = "<UUID>"
2. バインディングを設定する

wrangler.jsonc に追加します:

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-11",
  "kv_namespaces": [
    {
      "binding": "MY_NAMESPACE",          // Available as env.MY_NAMESPACE
      "id": "<production-uuid>",           // Production namespace ID
      "preview_id": "<preview-uuid>"       // Local dev namespace ID (optional)
    }
  ]
}

または wrangler.toml を使用します:

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2025-10-11"

[[kv_namespaces]]
binding = "MY_NAMESPACE"
id = "<production-uuid>"
preview_id = "<preview-uuid>"  # optional

重要:

  • binding は、コード内で名前空間にアクセスする方法です(env.MY_NAMESPACE)
  • id は本番環境の名前空間 UUID です
  • preview_id はローカル開発用です(省略可能。別の名前空間を使用します)
  • 実際の名前空間 ID を公開リポジトリにコミットしないでください。環境変数またはシークレットを使用してください
3. 最初のキーと値のペアを書き込む
import { Hono } from 'hono';

type Bindings = {
  MY_NAMESPACE: KVNamespace;
};

const app = new Hono<{ Bindings: Bindings }>();

app.post('/set/:key', async (c) => {
  const key = c.req.param('key');
  const value = await c.req.text();

  // Simple write
  await c.env.MY_NAMESPACE.put(key, value);

  return c.json({ success: true, key });
});

app.get('/get/:key', async (c) => {
  const key = c.req.param('key');
  const value = await c.env.MY_NAMESPACE.get(key);

  if (!value) {
    return c.json({ error: 'Not found' }, 404);
  }

  return c.json({ value });
});

export default app;
4. ローカルでテストする
# Start local development server
npm run dev

# In another terminal, test the endpoints
curl -X POST http://localhost:8787/set/test -d "Hello KV"
# {"success":true,"key":"test"}

curl http://localhost:8787/get/test
# {"value":"Hello KV"}

Workers KV API 完全ガイド

1. 読み取り操作
get() - 単一のキーを読み取る
// Get as string (default)
const value: string | null = await env.MY_KV.get('my-key');

// Get as JSON
const data: MyType | null = await env.MY_KV.get('my-key', { type: 'json' });

// Get as ArrayBuffer
const buffer: ArrayBuffer | null = await env.MY_KV.get('my-key', { type: 'arrayBuffer' });

// Get as ReadableStream
const stream: ReadableStream | null = await env.MY_KV.get('my-key', { type: 'stream' });

// Get with cache optimization
const value = await env.MY_KV.get('my-key', {
  type: 'text',
  cacheTtl: 300, // Cache at edge for 5 minutes (minimum 60 seconds)
});
get() - 複数のキーを読み取る(一括)
// Read multiple keys at once (counts as 1 operation)
const keys = ['key1', 'key2', 'key3'];
const values: Map<string, string | null> = await env.MY_KV.get(keys);

// Access values
const value1 = values.get('key1'); // string | null
const value2 = values.get('key2'); // string | null

// Convert to object
const obj = Object.fromEntries(values);
getWithMetadata() - メタデータとともに読み取る
// Get single key with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('my-key');

// value: string | null
// metadata: any | null

// Get as JSON with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata<MyType>('my-key', {
  type: 'json',
  cacheTtl: 300,
});

// Get multiple keys with metadata
const keys = ['key1', 'key2'];
const result: Map<string, { value: string | null, metadata: any | null }> =
  await env.MY_KV.getWithMetadata(keys);

for (const [key, data] of result) {
  console.log(key, data.value, data.metadata);
}

型のオプション:

  • text(デフォルト)- string を返します
  • json - JSON を解析し、object を返します
  • arrayBuffer - ArrayBuffer を返します
  • stream - ReadableStream を返します

注: get(keys[]) による一括読み取りでサポートされる型は text と json のみです。arrayBuffer または stream を使用する場合は、Promise.all() と個別の get() 呼び出しを使用してください。


2. 書き込み操作
put() - キーと値のペアを書き込む
// Simple write
await env.MY_KV.put('key', 'value');

// Write JSON
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John', age: 30 }));

// Write with expiration (TTL)
await env.MY_KV.put('session:abc', sessionData, {
  expirationTtl: 3600, // Expire in 1 hour (minimum 60 seconds)
});

// Write with absolute expiration
const expirationTime = Math.floor(Date.now() / 1000) + 86400; // 24 hours from now
await env.MY_KV.put('token', tokenValue, {
  expiration: expirationTime, // Seconds since epoch
});

// Write with metadata
await env.MY_KV.put('config:theme', 'dark', {
  metadata: {
    updatedAt: Date.now(),
    updatedBy: 'admin',
    version: 2
  },
});

// Write with everything
await env.MY_KV.put('feature:flags', JSON.stringify(flags), {
  expirationTtl: 600,
  metadata: { source: 'api', timestamp: Date.now() },
});

重要な制限:

  • キーのサイズ:最大 512 バイト
  • 値のサイズ:最大 25 MiB
  • メタデータのサイズ:最大 1024 バイト(JSON シリアライズ後)
  • 書き込み頻度:キーごとに最大 1 秒あたり 1 回
  • 有効期限の最小値:60 秒(TTL と絶対時刻の両方)

レート制限への対処:

async function putWithRetry(
  kv: KVNamespace,
  key: string,
  value: string,
  options?: KVPutOptions
) {
  let attempts = 0;
  const maxAttempts = 5;
  let delay = 1000; // Start with 1 second

  while (attempts < maxAttempts) {
    try {
      await kv.put(key, value, options);
      return; // Success
    } catch (error) {
      const message = (error as Error).message;

      if (message.includes('429') || message.includes('Too Many Requests')) {
        attempts++;
        if (attempts >= maxAttempts) {
          throw new Error('Max retry attempts reached');
        }

        console.warn(`Attempt ${attempts} failed. Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));

        // Exponential backoff
        delay *= 2;
      } else {
        throw error; // Different error, rethrow
      }
    }
  }
}

3. リスト操作
list() - キーを一覧表示する
// List all keys (up to 1000)
const result = await env.MY_KV.list();

console.log(result.keys);         // Array of key objects
console.log(result.list_complete); // boolean - false if more keys exist
console.log(result.cursor);        // string - for pagination

// List with prefix filter
const result = await env.MY_KV.list({
  prefix: 'user:', // Only keys starting with 'user:'
});

// List with limit
const result = await env.MY_KV.list({
  limit: 100, // Maximum 1000 (default 1000)
});

// Pagination with cursor
let cursor: string | undefined;
let allKeys: any[] = [];

do {
  const result = await env.MY_KV.list({ cursor });
  allKeys = allKeys.concat(result.keys);
  cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);

// Combined: prefix + pagination
let cursor: string | undefined;
const userKeys: any[] = [];

do {
  const result = await env.MY_KV.list({
    prefix: 'user:',
    cursor,
  });

  userKeys.push(...result.keys);
  cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);

リスト応答の形式:

{
  keys: [
    {
      name: "user:123",
      expiration: 1234567890,  // Optional: seconds since epoch
      metadata: { ... }         // Optional: metadata object
    },
    // ... more keys
  ],
  list_complete: false,  // true if no more keys
  cursor: "6Ck1la0VxJ0djhidm1MdX2FyD"  // Use for next page
}

重要:

  • キーは常に辞書順(UTF-8)で返されます
  • keys.length === 0 ではなく、必ず list_complete を確認してください
  • keys 配列が空でも、データがそれ以上ないとは限りません(期限切れまたは削除済みのキーによって「トゥームストーン」が作成されます)
  • prefix を使ってページネーションする場合、cursor リクエストごとに同じ prefix を渡す必要があります

4. 削除操作
delete() - キーを削除する
// Delete single key
await env.MY_KV.delete('my-key');

// Delete always succeeds, even if key doesn't exist
await env.MY_KV.delete('non-existent-key'); // No error

// Bulk delete pattern (in Worker)
const keysToDelete = ['key1', 'key2', 'key3', ...];

// Delete in parallel (careful of Worker subrequest limits)
await Promise.all(
  keysToDelete.map(key => env.MY_KV.delete(key))
);

// For more than 1000 keys, use REST API bulk delete (via wrangler or API)

REST API による一括削除:

Workers バインディングは一括削除をサポートしていませんが、REST API(wrangler または直接の API 呼び出し)を使用できます:

# Using wrangler CLI
npx wrangler kv bulk delete --namespace-id=<UUID> keys.json

# keys.json format:
# ["key1", "key2", "key3"]

REST API の制限: 一括削除リクエストあたり最大 10,000 個のキー。


高度なパターンとベストプラクティス

1. CacheTtl を使ったキャッシュパターン
async function getCachedData(
  kv: KVNamespace,
  cacheKey: string,
  fetchFn: () => Promise<any>,
  cacheTtl: number = 300
) {
  // Try to get from KV cache
  const cached = await kv.get(cacheKey, {
    type: 'json',
    cacheTtl, // Cache at edge for faster subsequent reads
  });

  if (cached) {
    return cached;
  }

  // Cache miss - fetch fresh data
  const data = await fetchFn();

  // Store in KV with expiration
  await kv.put(cacheKey, JSON.stringify(data), {
    expirationTtl: cacheTtl * 2, // Store longer than cache
  });

  return data;
}

// Usage
app.get('/api/data/:id', async (c) => {
  const id = c.req.param('id');

  const data = await getCachedData(
    c.env.CACHE,
    `data:${id}`,
    () => fetchFromDatabase(id),
    300 // 5 minutes
  );

  return c.json(data);
});

CacheTtl のガイドライン:

  • 最小値: 60 秒
  • デフォルト: 60 秒
  • 最大値: Number.MAX_SAFE_INTEGER
  • 用途: 頻繁に読み取られ、更新頻度の低いデータ
  • トレードオフ: CacheTtl を大きくすると読み取りは速くなりますが、更新の反映は遅くなります

2. メタデータ最適化パターン

個別の get() 呼び出しを避けるため、小さな値をメタデータに保存します:

// ❌ Bad: Two operations
await env.MY_KV.put('user:123', 'active');
const status = await env.MY_KV.get('user:123');

// ✅ Good: Store in metadata with empty value
await env.MY_KV.put('user:123', '', {
  metadata: {
    status: 'active',
    lastSeen: Date.now(),
    plan: 'pro'
  },
});

// List returns metadata automatically
const users = await env.MY_KV.list({ prefix: 'user:' });

users.keys.forEach(({ name, metadata }) => {
  console.log(name, metadata.status, metadata.plan);
  // No additional get() calls needed!
});

使用する場面:

  • ✅ 値が 1024 バイト以内に収まる
  • ✅ list() 操作を頻繁に使用する
  • ✅ 多数のキーをフィルタリングまたは処理する必要がある
  • ❌ 大きな値には使用しないでください(通常の値ストレージを使用します)

3. パフォーマンス向上のためのキーの集約

アクセス頻度の低いキーを、アクセス頻度の高いキーとまとめます:

// ❌ Bad: Many individual keys (some hot, some cold)
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', '[email protected]');
await kv.put('user:123:age', '30');

// ✅ Good: Coalesce into single hot key
await kv.put('user:123', JSON.stringify({
  name: 'John',
  email: '[email protected]',
  age: 30,
}));

// Single read gets everything
const user = await kv.get<User>('user:123', { type: 'json' });

メリット:

  • アクセス頻度の低いキーも、アクセス頻度の高いキーのキャッシュの恩恵を受けます
  • 操作回数が減り、パフォーマンスが向上します
  • 複数ではなく、キャッシュエントリーを 1 つにできます

デメリット:

  • 個々のフィールドを簡単に更新できません(読み取り、変更、書き込みが必要です)
  • 集約した値が大きいと、メモリ制限に達する可能性があります
  • 同時更新にはロック機構が必要です

4. ページネーションヘルパー
async function* paginateKV(
  kv: KVNamespace,
  options: { prefix?: string; limit?: number } = {}
) {
  let cursor: string | undefined;

  do {
    const result = await kv.list({
      prefix: options.prefix,
      limit: options.limit || 1000,
      cursor,
    });

    yield result.keys;

    cursor = result.list_complete ? undefined : result.cursor;
  } while (cursor);
}

// Usage
app.get('/all-users', async (c) => {
  const allUsers = [];

  for await (const keys of paginateKV(c.env.MY_KV, { prefix: 'user:' })) {
    // Process batch
    allUsers.push(...keys.map(k => k.name));
  }

  return c.json({ users: allUsers, count: allUsers.length });
});

5. フラグ機能のパターン
interface FeatureFlags {
  darkMode: boolean;
  newDashboard: boolean;
  betaFeatures: boolean;
}

async function getFeatureFlags(
  kv: KVNamespace,
  userId?: string
): Promise<FeatureFlags> {
  // Try user-specific flags first
  if (userId) {
    const userFlags = await kv.get<FeatureFlags>(`flags:user:${userId}`, {
      type: 'json',
      cacheTtl: 300,
    });
    if (userFlags) return userFlags;
  }

  // Fallback to global flags
  const globalFlags = await kv.get<FeatureFlags>('flags:global', {
    type: 'json',
    cacheTtl: 300,
  });

  return globalFlags || {
    darkMode: false,
    newDashboard: false,
    betaFeatures: false,
  };
}

// Update global flags
app.post('/admin/flags', async (c) => {
  const flags = await c.req.json<FeatureFlags>();

  await c.env.CONFIG.put('flags:global', JSON.stringify(flags), {
    metadata: { updatedAt: Date.now() },
  });

  return c.json({ success: true });
});

結果整合性について

KV は Cloudflare のグローバルネットワーク全体で結果整合性を持ちます:

仕組み:
  1. 書き込みは同じロケーション内ですぐに反映されます
  2. 他のロケーションには、約 60 秒以内(または指定した cacheTtl の値)に更新が反映されます
  3. キャッシュされた読み取りでは、反映中に古いデータが返されることがあります
影響:
// In Tokyo data center:
await env.MY_KV.put('counter', '1');
const value1 = await env.MY_KV.get('counter'); // "1" ✅

// In London data center (within 60 seconds):
const value2 = await env.MY_KV.get('counter'); // Might still be old value ⚠️

// After 60+ seconds:
const value3 = await env.MY_KV.get('counter'); // "1" ✅
ベストプラクティス:

✅ KV を使うケース:

  • 読み取り中心のワークロード(読み取り/書き込み比が100:1)
  • グローバルで即時の整合性を必要としないデータ
  • 設定、フィーチャーフラグ、キャッシュ
  • ユーザー設定、セッションデータ

❌ KV を使わないケース:

  • アトミック操作を必要とする金融取引
  • 強い整合性を必要とするデータ
  • 同じキーへの高頻度の書き込み(1秒あたり1回超)
  • 古いデータの読み取りが許容されない重要なデータ

強い整合性が必要な場合は、Durable Objects を使ってください。


Wrangler CLI の操作

名前空間の作成
# Production namespace
npx wrangler kv namespace create MY_NAMESPACE

# Preview/development namespace
npx wrangler kv namespace create MY_NAMESPACE --preview
名前空間の一覧表示
npx wrangler kv namespace list
キーと値のペアの書き込み
# Write single key
npx wrangler kv key put --binding=MY_NAMESPACE "my-key" "my-value"

# Write from file
npx wrangler kv key put --binding=MY_NAMESPACE "config" --path=config.json

# Write with metadata
npx wrangler kv key put --binding=MY_NAMESPACE "key" "value" --metadata='{"version":1}'

# Write with TTL (seconds)
npx wrangler kv key put --binding=MY_NAMESPACE "session" "data" --ttl=3600
キーと値のペアの読み取り
# Read single key
npx wrangler kv key get --binding=MY_NAMESPACE "my-key"

# Read to file
npx wrangler kv key get --binding=MY_NAMESPACE "image" --path=image.png
キーの一覧表示
# List all keys
npx wrangler kv key list --binding=MY_NAMESPACE

# List with prefix
npx wrangler kv key list --binding=MY_NAMESPACE --prefix="user:"

# Pretty print
npx wrangler kv key list --binding=MY_NAMESPACE | jq "."
キーの削除
# Delete single key
npx wrangler kv key delete --binding=MY_NAMESPACE "my-key"
一括操作
# Bulk write (up to 10,000 keys)
npx wrangler kv bulk put --binding=MY_NAMESPACE data.json

# data.json format:
# [
#   {"key": "key1", "value": "value1"},
#   {"key": "key2", "value": "value2", "expiration_ttl": 3600}
# ]

# Bulk delete (up to 10,000 keys)
npx wrangler kv bulk delete --binding=MY_NAMESPACE keys.json

# keys.json format:
# ["key1", "key2", "key3"]

制限とクォータ

機能無料プラン有料プラン
1日あたりの読み取り回数100,000無制限
1日あたりの書き込み回数(異なるキー)1,000無制限
キーあたりの1秒間の書き込み回数11
Worker の呼び出しあたりの操作数1,0001,000
アカウントあたりの名前空間数1,0001,000
アカウントあたりのストレージ容量1 GB無制限
名前空間あたりのストレージ容量1 GB無制限
名前空間あたりのキー数無制限無制限
キーのサイズ512 bytes512 bytes
メタデータのサイズ1024 bytes1024 bytes
値のサイズ25 MiB25 MiB
cacheTtl の最小値60 seconds60 seconds
cacheTtl の最大値Number.MAX_SAFE_INTEGERNumber.MAX_SAFE_INTEGER

重要な注意事項:

  • キーあたり1秒に1回の書き込み:同じキーへの同時書き込みは429エラーを引き起こします
  • 呼び出しあたり1000操作:一括操作は1操作として数えられます
  • 一括読み取り(複数キーの読み取り)は1操作として数えられます
  • REST API にはCloudflare API のレート制限が適用されます

TypeScript の型

// KVNamespace type is provided by @cloudflare/workers-types
interface KVNamespace {
  get(key: string, options?: Partial<KVGetOptions<undefined>>): Promise<string | null>;
  get(key: string, type: "text"): Promise<string | null>;
  get<ExpectedValue = unknown>(key: string, type: "json"): Promise<ExpectedValue | null>;
  get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
  get(key: string, type: "stream"): Promise<ReadableStream | null>;
  get(key: string, options?: KVGetOptions<"text">): Promise<string | null>;
  get<ExpectedValue = unknown>(key: string, options?: KVGetOptions<"json">): Promise<ExpectedValue | null>;
  get(key: string, options?: KVGetOptions<"arrayBuffer">): Promise<ArrayBuffer | null>;
  get(key: string, options?: KVGetOptions<"stream">): Promise<ReadableStream | null>;
  get(keys: string[]): Promise<Map<string, string | null>>;
  get(keys: string[], type: "text"): Promise<Map<string, string | null>>;
  get<ExpectedValue = unknown>(keys: string[], type: "json"): Promise<Map<string, ExpectedValue | null>>;

  getWithMetadata<Metadata = unknown>(key: string, options?: Partial<KVGetOptions<undefined>>): Promise<KVGetWithMetadataResult<string, Metadata>>;
  getWithMetadata<Metadata = unknown>(key: string, type: "text"): Promise<KVGetWithMetadataResult<string, Metadata>>;
  getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: string, type: "json"): Promise<KVGetWithMetadataResult<ExpectedValue, Metadata>>;
  getWithMetadata<Metadata = unknown>(key: string, options?: KVGetOptions<"text">): Promise<KVGetWithMetadataResult<string, Metadata>>;
  getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: string, options?: KVGetOptions<"json">): Promise<KVGetWithMetadataResult<ExpectedValue, Metadata>>;
  getWithMetadata<Metadata = unknown>(keys: string[]): Promise<Map<string, KVGetWithMetadataResult<string, Metadata>>>;
  getWithMetadata<Metadata = unknown>(keys: string[], type: "text"): Promise<Map<string, KVGetWithMetadataResult<string, Metadata>>>;
  getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(keys: string[], type: "json"): Promise<Map<string, KVGetWithMetadataResult<ExpectedValue, Metadata>>>;

  put(key: string, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVPutOptions): Promise<void>;

  delete(key: string): Promise<void>;

  list<Metadata = unknown>(options?: KVListOptions): Promise<KVListResult<Metadata>>;
}

interface KVGetOptions<Type> {
  type: Type;
  cacheTtl?: number;
}

interface KVGetWithMetadataResult<Value, Metadata> {
  value: Value | null;
  metadata: Metadata | null;
}

interface KVPutOptions {
  expiration?: number;        // Seconds since epoch
  expirationTtl?: number;     // Seconds from now (minimum 60)
  metadata?: any;             // Serializable to JSON, max 1024 bytes
}

interface KVListOptions {
  prefix?: string;
  limit?: number;   // Default 1000, max 1000
  cursor?: string;
}

interface KVListResult<Metadata = unknown> {
  keys: {
    name: string;
    expiration?: number;
    metadata?: Metadata;
  }[];
  list_complete: boolean;
  cursor?: string;
}

エラー処理

よくあるエラー
1. レート制限(429 Too Many Requests)
try {
  await env.MY_KV.put('counter', '1');
  await env.MY_KV.put('counter', '2'); // Too fast! < 1 second
} catch (error) {
  // Error: KV PUT failed: 429 Too Many Requests
  console.error(error);
}

// Solution: Use retry with backoff (see putWithRetry example above)
2. 値が大きすぎる
const largeValue = 'x'.repeat(26 * 1024 * 1024); // > 25 MiB

try {
  await env.MY_KV.put('large', largeValue);
} catch (error) {
  // Error: Value too large
  console.error(error);
}

// Solution: Check size before writing
if (value.length > 25 * 1024 * 1024) {
  throw new Error('Value exceeds 25 MiB limit');
}
3. メタデータが大きすぎる
const metadata = { data: 'x'.repeat(2000) }; // > 1024 bytes serialized

try {
  await env.MY_KV.put('key', 'value', { metadata });
} catch (error) {
  // Error: Metadata too large
  console.error(error);
}

// Solution: Validate metadata size
const serialized = JSON.stringify(metadata);
if (serialized.length > 1024) {
  throw new Error('Metadata exceeds 1024 byte limit');
}
4. 無効な CacheTtl
// ❌ Too low
await env.MY_KV.get('key', { cacheTtl: 30 }); // Error: minimum is 60

// ✅ Correct
await env.MY_KV.get('key', { cacheTtl: 60 });

必ず行うこと ✅

  1. 複数のキーを読み取る場合は一括操作を使う(1操作として数えられます)
  2. 頻繁に読み取られ、更新頻度の低いデータにはcacheTtl を設定する
  3. list() を頻繁に使う場合は、小さな値をメタデータに保存する
  4. ページネーション時は、keys.length === 0 ではなく**list_complete を確認する**
  5. 書き込み操作には指数バックオフを使った再試行ロジックを使う
  6. 書き込み前にサイズを検証する(キー512 bytes、値25 MiB、メタデータ1 KB)
  7. ローカル開発にはプレビュー名前空間を使う
  8. キャッシュ無効化には適切な TTL を設定する(最小60秒)
  9. キャッシュ性能を向上させるため、関連するキーをまとめる
  10. 読み取り中心のワークロードに KV を使う(読み取り/書き込み比100:1が理想的)

絶対に行わないこと ❌

  1. 同じキーに1秒あたり1回を超えて書き込まない - 429のレート制限エラーが発生します
  2. 即時のグローバル整合性を前提にしない - 反映には約60秒かかります
  3. アトミック操作に KV を使わない - 代わりに Durable Objects を使います
  4. cacheTtl を60秒未満に設定しない - 失敗します
  5. 名前空間 ID を公開リポジトリにコミットしない - 環境変数を使います
  6. 呼び出しあたり1000操作を超えない - 一括操作を使います
  7. 書き込み順序を当てにしない - 結果整合性のため、保証はありません
  8. 暗号化せずに機密データを保存しない - KV はデフォルトでは保存時に暗号化されません
  9. 高頻度の書き込みに KV を使わない - 書き込み中心のワークロード向けには設計されていません
  10. null 値の処理を忘れない - キーが存在しない場合、get() は null を返します

トラブルシューティング

問題:「429 Too Many Requests」が書き込み時に発生する

原因: 同じキーに1秒あたり1回を超えて書き込んでいる

解決策:

// ❌ Bad
for (let i = 0; i < 10; i++) {
  await kv.put('counter', String(i)); // Rate limit!
}

// ✅ Good - consolidate writes
const finalValue = '9';
await kv.put('counter', finalValue);

// ✅ Good - use retry with backoff
await putWithRetry(kv, 'counter', String(i));

問題:書き込み後に古いデータが読み取られる

原因: 結果整合性により、書き込みが全世界に反映されるまで最大60秒かかります。

解決策:

// Accept that reads may be stale for up to 60 seconds
// OR use Durable Objects for strong consistency
// OR implement application-level cache invalidation

問題:「Operations limit exceeded」

原因: 1回のWorkerの実行で1000件を超えるKV操作が行われています。

解決策:

// ❌ Bad - 5000 operations
const keys = Array.from({ length: 5000 }, (_, i) => `key${i}`);
for (const key of keys) {
  await kv.get(key); // Exceeds 1000 limit
}

// ✅ Good - 1 operation (bulk read)
const values = await kv.get(keys);

問題:リストが空なのにカーソルが存在する

原因: 最近削除されたキーや期限切れのキーにより、リスト内に「トゥームストーン」が作成されています。

解決策:

// Always check list_complete, not keys.length
let cursor: string | undefined;

do {
  const result = await kv.list({ cursor });

  // Process keys even if empty
  processKeys(result.keys);

  // CORRECT: Check list_complete
  cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);

本番環境へのデプロイ前チェックリスト

本番環境にデプロイする前に、以下を確認してください。

  • 環境ごとの名前空間が設定されている(id と preview_id)
  • 名前空間IDが環境変数に保存されている(ハードコードされていない)
  • 書き込み時のレート制限に対する再試行ロジックが実装されている
  • 読み取りに適切な cacheTtl 値が設定されている
  • メタデータのサイズが検証されている(<1024 bytes)
  • 値のサイズが検証されている(<25 MiB)
  • キーのサイズが検証されている(<512 bytes)
  • 可能な場合は一括操作が使用されている
  • list() のページネーションが正しく実装されている
  • null 値に対するエラー処理が行われている
  • レート制限に対する監視/アラートが設定されている
  • 結果整合性の動作に関するドキュメントが用意されている

関連ドキュメント


最終更新日:2025-10-21 バージョン:1.0.0 メンテナー:Jeremy Dawes | [email protected]

コメント