Skip to main content
Technology & EngineeringCloudflare Workers320 lines

Workers KV

Cloudflare Workers KV namespace for globally distributed key-value storage, including read/write patterns, caching strategies, TTL, list operations, metadata, bulk operations, and the eventual consistency model.

Quick Summary31 lines
You are an expert in Cloudflare Workers KV, a globally distributed, eventually consistent key-value store optimized for high-read, low-write workloads at the edge.

## Key Points

- **Read-after-write in the same location**: Immediately consistent.
- **Global propagation**: Eventually consistent, typically within 60 seconds.
- **Conflict resolution**: Last-writer-wins. There are no transactions or compare-and-swap operations.
- **Caching**: Reads are cached at the edge. Frequently-read keys are served from the nearest cache, making reads extremely fast (sub-millisecond).
- **Stale reads after writing**: Expected behavior due to eventual consistency. If you need immediate read-after-write consistency globally, use Durable Objects.
- **Key not found after put**: Ensure you are not reading from a different namespace (check preview vs production IDs).
- **Value too large**: KV values max out at 25 MiB. For larger objects, use R2.
- **List operations slow**: `list()` calls are not cached and can be slow for large namespaces. Use prefixes to narrow results.

## Quick Example

```bash
# Create namespace
npx wrangler kv namespace create MY_KV

# Create a preview namespace for local dev
npx wrangler kv namespace create MY_KV --preview
```

```toml
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456"
preview_id = "789ghi012jkl"
```
skilldb get cloudflare-workers-skills/Workers KVFull skill: 320 lines
Paste into your CLAUDE.md or agent config

Workers KV — Cloudflare Workers

You are an expert in Cloudflare Workers KV, a globally distributed, eventually consistent key-value store optimized for high-read, low-write workloads at the edge.

Core Philosophy

Overview

KV is designed for read-heavy use cases — configuration, feature flags, static assets, session data, and cached API responses. Writes propagate globally within 60 seconds (usually faster). KV is not suitable for data that requires strong consistency or frequent writes from multiple locations.

Consistency model

  • Read-after-write in the same location: Immediately consistent.
  • Global propagation: Eventually consistent, typically within 60 seconds.
  • Conflict resolution: Last-writer-wins. There are no transactions or compare-and-swap operations.
  • Caching: Reads are cached at the edge. Frequently-read keys are served from the nearest cache, making reads extremely fast (sub-millisecond).

Setup

Create a KV namespace

# Create namespace
npx wrangler kv namespace create MY_KV

# Create a preview namespace for local dev
npx wrangler kv namespace create MY_KV --preview

Bind in wrangler.toml

[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456"
preview_id = "789ghi012jkl"

TypeScript binding

export interface Env {
  MY_KV: KVNamespace;
}

Basic Operations

Writing values

// Simple string write
await env.MY_KV.put("user:1234", JSON.stringify({ name: "Alice", role: "admin" }));

// Write with TTL (expires in 1 hour)
await env.MY_KV.put("session:abc", sessionData, { expirationTtl: 3600 });

// Write with absolute expiration (Unix timestamp in seconds)
const oneHourFromNow = Math.floor(Date.now() / 1000) + 3600;
await env.MY_KV.put("token:xyz", tokenData, { expiration: oneHourFromNow });

// Write with metadata (stored alongside the value)
await env.MY_KV.put("page:/about", htmlContent, {
  metadata: { contentType: "text/html", version: 3, updatedAt: Date.now() },
});

Reading values

// Read as string (default)
const value = await env.MY_KV.get("user:1234");
if (value === null) {
  // Key does not exist
}

// Read as JSON (auto-parsed)
const user = await env.MY_KV.get("user:1234", { type: "json" });

// Read as ArrayBuffer (binary data)
const imageData = await env.MY_KV.get("image:logo", { type: "arrayBuffer" });

// Read as ReadableStream (efficient for large values)
const stream = await env.MY_KV.get("large-file", { type: "stream" });
if (stream) {
  return new Response(stream, {
    headers: { "content-type": "application/octet-stream" },
  });
}

Reading with metadata

// getWithMetadata returns both value and metadata
const result = await env.MY_KV.getWithMetadata("page:/about", { type: "text" });

if (result.value !== null) {
  const { value, metadata } = result;
  return new Response(value, {
    headers: { "content-type": metadata?.contentType || "text/plain" },
  });
}

Deleting values

await env.MY_KV.delete("user:1234");
// Delete is idempotent — deleting a non-existent key does not error

List Operations

Listing keys

// List all keys (up to 1000 per call)
const list = await env.MY_KV.list();
// list.keys: Array<{ name: string, expiration?: number, metadata?: unknown }>
// list.list_complete: boolean
// list.cursor: string (for pagination)

// List with prefix filter
const userKeys = await env.MY_KV.list({ prefix: "user:" });

// List with limit
const firstTen = await env.MY_KV.list({ prefix: "user:", limit: 10 });

// Paginate through all keys
async function listAllKeys(kv: KVNamespace, prefix: string): Promise<string[]> {
  const keys: string[] = [];
  let cursor: string | undefined;

  do {
    const result = await kv.list({ prefix, cursor });
    keys.push(...result.keys.map((k) => k.name));
    cursor = result.list_complete ? undefined : result.cursor;
  } while (cursor);

  return keys;
}

Bulk Operations (via Wrangler CLI)

# Bulk write from a JSON file
# File format: [{"key": "k1", "value": "v1"}, {"key": "k2", "value": "v2"}]
npx wrangler kv bulk put --namespace-id=abc123 ./data.json

# Bulk delete
# File format: ["key1", "key2", "key3"]
npx wrangler kv bulk delete --namespace-id=abc123 ./keys.json

# Read a single key via CLI
npx wrangler kv key get --namespace-id=abc123 "user:1234"

# Write a single key via CLI
npx wrangler kv key put --namespace-id=abc123 "user:1234" '{"name":"Alice"}'

Caching Strategies

Cache-aside pattern

async function getCachedData(env: Env, key: string): Promise<unknown> {
  // Try KV first
  const cached = await env.MY_KV.get(key, { type: "json" });
  if (cached !== null) {
    return cached;
  }

  // Fetch from origin
  const fresh = await fetchFromOrigin(key);

  // Store in KV with TTL
  await env.MY_KV.put(key, JSON.stringify(fresh), { expirationTtl: 300 });

  return fresh;
}

Stale-while-revalidate pattern

interface CachedEntry<T> {
  data: T;
  fetchedAt: number;
}

async function getWithSWR<T>(
  env: Env,
  ctx: ExecutionContext,
  key: string,
  fetcher: () => Promise<T>,
  maxAgeMs: number = 60_000, // Fresh for 1 minute
  staleAgeMs: number = 300_000 // Stale but usable for 5 minutes
): Promise<T> {
  const cached = await env.MY_KV.get<CachedEntry<T>>(key, { type: "json" });
  const now = Date.now();

  if (cached) {
    const age = now - cached.fetchedAt;

    if (age < maxAgeMs) {
      // Fresh — return immediately
      return cached.data;
    }

    if (age < staleAgeMs) {
      // Stale — return but revalidate in background
      ctx.waitUntil(revalidate(env, key, fetcher));
      return cached.data;
    }
  }

  // Expired or missing — fetch synchronously
  return revalidate(env, key, fetcher);
}

async function revalidate<T>(
  env: Env,
  key: string,
  fetcher: () => Promise<T>
): Promise<T> {
  const data = await fetcher();
  const entry: CachedEntry<T> = { data, fetchedAt: Date.now() };
  await env.MY_KV.put(key, JSON.stringify(entry), { expirationTtl: 3600 });
  return data;
}

Feature flags

interface FeatureFlags {
  newCheckout: boolean;
  darkMode: boolean;
  betaAPI: boolean;
}

async function getFeatureFlags(env: Env): Promise<FeatureFlags> {
  const flags = await env.MY_KV.get<FeatureFlags>("config:feature-flags", { type: "json" });
  return flags ?? { newCheckout: false, darkMode: false, betaAPI: false };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const flags = await getFeatureFlags(env);

    if (flags.betaAPI) {
      return handleBetaAPI(request, env);
    }
    return handleStableAPI(request, env);
  },
};

Key Design Patterns

Key naming conventions

// Use colons as separators for hierarchical keys
"user:1234"
"user:1234:profile"
"user:1234:settings"
"session:abc-def-123"
"cache:api:/v1/products?page=1"
"config:feature-flags"
"page:/blog/my-post"

Storing structured data

// Always serialize to JSON for objects
await env.MY_KV.put("user:1234", JSON.stringify({
  id: "1234",
  name: "Alice",
  email: "alice@example.com",
  createdAt: "2024-01-15T00:00:00Z",
}));

// Use metadata for indexing/filtering without reading the full value
await env.MY_KV.put("product:shoe-1", JSON.stringify(largeProductData), {
  metadata: { category: "shoes", price: 99.99, inStock: true },
});

Atomic counter alternative (use Durable Objects instead)

KV does not support atomic increments. For counters, rate limiting, or any operation requiring read-modify-write atomicity, use Durable Objects instead. KV's last-writer-wins semantics will lose increments under concurrent writes.

Limits and Quotas

ResourceFreePaid
Reads per day100,00010,000,000+
Writes per day1,0001,000,000+
Key size512 bytes512 bytes
Value size25 MiB25 MiB
Metadata size1024 bytes1024 bytes
Namespaces per account100100

Troubleshooting

  • Stale reads after writing: Expected behavior due to eventual consistency. If you need immediate read-after-write consistency globally, use Durable Objects.
  • Key not found after put: Ensure you are not reading from a different namespace (check preview vs production IDs).
  • Value too large: KV values max out at 25 MiB. For larger objects, use R2.
  • List operations slow: list() calls are not cached and can be slow for large namespaces. Use prefixes to narrow results.

Install this skill directly: skilldb add cloudflare-workers-skills

Get CLI access →