Agent Skills: Cloudflare Workers KV

Cloudflare Workers KV key-value storage playbook: namespaces, bindings, Workers API (get/put/delete/list), metadata, expiration TTL, bulk operations, REST API, consistency model, caching. Keywords: Cloudflare KV, Workers KV, key-value, KVNamespace, binding, metadata, expiration, TTL, cacheTtl, bulk operations, eventually consistent.

UncategorizedID: itechmeat/llm-code/cloudflare-kv

Install this agent skill to your local

pnpm dlx add-skill https://github.com/itechmeat/llm-code/tree/HEAD/skills/cloudflare-kv

Skill Files

Browse the full folder contents for cloudflare-kv.

Download Skill

Loading file tree…

skills/cloudflare-kv/SKILL.md

Skill Metadata

Name
cloudflare-kv
Description
"Cloudflare Workers KV key-value storage playbook: namespaces, bindings, Workers API (get/put/delete/list), metadata, expiration TTL, bulk operations, REST API, consistency model, caching. Keywords: Cloudflare KV, Workers KV, key-value, KVNamespace, binding, metadata, expiration, TTL, cacheTtl, bulk operations, eventually consistent."

Cloudflare Workers KV

KV is a global, low-latency, eventually-consistent key-value store. Optimized for high-read, low-write workloads.


Quick Start

Create namespace

npx wrangler kv namespace create MY_KV

Add binding

// wrangler.jsonc
{
  "kv_namespaces": [
    {
      "binding": "MY_KV",
      "id": "06779da6940b431db6e566b4846d64db"
    }
  ]
}

Basic Worker

export interface Env {
  MY_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    await env.MY_KV.put("user:123", JSON.stringify({ name: "Alice" }));
    const user = await env.MY_KV.get("user:123", "json");
    const keys = await env.MY_KV.list({ prefix: "user:" });
    await env.MY_KV.delete("user:123");
    return Response.json({ user, keys: keys.keys });
  },
};

Binding Configuration

// wrangler.jsonc
{
  "kv_namespaces": [
    {
      "binding": "MY_KV", // Variable name in env
      "id": "namespace-uuid", // Namespace ID
      "preview_id": "preview-uuid" // Optional: for local dev
    }
  ]
}
# wrangler.toml
[[kv_namespaces]]
binding = "MY_KV"
id = "namespace-uuid"
preview_id = "preview-uuid"

Remote binding (use production in dev)

{
  "kv_namespaces": [
    {
      "binding": "MY_KV",
      "id": "namespace-uuid",
      "remote": true
    }
  ]
}

See binding.md for details.


Workers API

put(key, value, options?)

await env.MY_KV.put("key", "value");

// With options
await env.MY_KV.put("key", "value", {
  expirationTtl: 3600, // Expires in 1 hour
  metadata: { version: 1 },
});

// With absolute expiration
await env.MY_KV.put("key", "value", {
  expiration: Math.floor(Date.now() / 1000) + 86400, // Unix timestamp
});

Value types: string | ReadableStream | ArrayBuffer

Options: | Option | Type | Description | |--------|------|-------------| | expirationTtl | number | Seconds from now (min 60) | | expiration | number | Unix timestamp (seconds) | | metadata | object | JSON-serializable (max 1024 bytes) |

get(key, type?)

// Text (default)
const text = await env.MY_KV.get("key");

// JSON
const obj = await env.MY_KV.get("key", "json");

// ArrayBuffer
const buffer = await env.MY_KV.get("key", "arrayBuffer");

// Stream
const stream = await env.MY_KV.get("key", "stream");

// With cache TTL
const cached = await env.MY_KV.get("key", { type: "json", cacheTtl: 300 });

Returns null if key doesn't exist.

get(keys[]) — Multi-key read

const results = await env.MY_KV.get(["key1", "key2", "key3"], "json");
// Returns Map<string, T | null>

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

Maximum 100 keys per call.

getWithMetadata(key, type?)

const { value, metadata } = await env.MY_KV.getWithMetadata("key", "json");
// value: T | null
// metadata: object | null

list(options?)

const result = await env.MY_KV.list();
// { keys: [...], list_complete: boolean, cursor?: string }

// With prefix
const users = await env.MY_KV.list({ prefix: "user:" });

// Pagination
let cursor: string | undefined;
do {
  const result = await env.MY_KV.list({ cursor, limit: 100 });
  for (const key of result.keys) {
    console.log(key.name, key.expiration, key.metadata);
  }
  cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);

Options: | Option | Type | Description | |--------|------|-------------| | prefix | string | Filter keys by prefix | | limit | number | Max keys (default/max: 1000) | | cursor | string | Pagination cursor |

delete(key)

await env.MY_KV.delete("key");
// Resolves even if key doesn't exist

See api.md for complete reference.


Expiration

Relative (TTL)

await env.MY_KV.put("session:abc", token, {
  expirationTtl: 3600, // 1 hour from now
});

Absolute

const expires = new Date("2025-01-01").getTime() / 1000;
await env.MY_KV.put("promo:holiday", data, {
  expiration: expires,
});

Minimum TTL: 60 seconds.

Expired keys are automatically deleted and not billed.


Metadata

Store up to 1024 bytes of JSON metadata per key.

// Write with metadata
await env.MY_KV.put("file:123", fileContent, {
  metadata: {
    filename: "document.pdf",
    contentType: "application/pdf",
    uploadedBy: "user-456",
  },
});

// Read with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata("file:123", "stream");

// Metadata in list results
const { keys } = await env.MY_KV.list({ prefix: "file:" });
for (const key of keys) {
  console.log(key.name, key.metadata);
}

Pattern: Store small values directly in metadata:

await env.MY_KV.put("config:theme", "", {
  metadata: { value: "dark", version: 2 },
});

// Read via list without get()
const { keys } = await env.MY_KV.list({ prefix: "config:" });
const theme = keys.find((k) => k.name === "config:theme")?.metadata?.value;

Cache TTL

Control how long reads are cached at edge locations.

const data = await env.MY_KV.get("static-config", {
  type: "json",
  cacheTtl: 3600, // Cache for 1 hour
});

Minimum: 60 seconds. Default: 60 seconds.

Use cases:

  • Increase for write-rarely data (static configs)
  • Keep low or default for frequently-updated data

Bulk Operations

Bulk operations via Wrangler or REST API only (not Workers binding).

Wrangler bulk write

# Create JSON file
cat > data.json << 'EOF'
[
  { "key": "user:1", "value": "{\"name\":\"Alice\"}" },
  { "key": "user:2", "value": "{\"name\":\"Bob\"}", "expiration_ttl": 3600 }
]
EOF

npx wrangler kv bulk put data.json --binding MY_KV

Wrangler bulk delete

cat > keys.json << 'EOF'
["user:1", "user:2", "user:3"]
EOF

npx wrangler kv bulk delete keys.json --binding MY_KV

Limits: 10,000 keys per request, 100 MB total.

See bulk.md for REST API examples.


Wrangler Commands

# Namespace
wrangler kv namespace create <NAME>
wrangler kv namespace list
wrangler kv namespace delete --namespace-id <ID>

# Key operations
wrangler kv key put <KEY> <VALUE> --binding <BINDING>
wrangler kv key get <KEY> --binding <BINDING>
wrangler kv key delete <KEY> --binding <BINDING>
wrangler kv key list --binding <BINDING> [--prefix <PREFIX>]

# With options
wrangler kv key put <KEY> <VALUE> --binding <BINDING> --ttl 3600
wrangler kv key put <KEY> --binding <BINDING> --path ./file.txt

# Bulk
wrangler kv bulk put <FILE.json> --binding <BINDING>
wrangler kv bulk delete <FILE.json> --binding <BINDING>

Consistency Model

KV is eventually consistent:

  • Writes propagate globally in ~60 seconds (or cacheTtl)
  • Writes are immediately visible at the origin location
  • Concurrent writes to same key: last write wins
  • Negative lookups (key not found) are cached

Write rate limit

1 write per second per key. Exceeding causes 429 errors.

// Implement retry with backoff
async function putWithRetry(key: string, value: string, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      await env.MY_KV.put(key, value);
      return;
    } catch (e) {
      if (i < retries - 1) await sleep(1000 * Math.pow(2, i));
      else throw e;
    }
  }
}

When to use Durable Objects instead

  • Need strong consistency
  • Need atomic operations
  • Need > 1 write/sec to same key
  • Need transactions

Limits

| Parameter | Free | Paid | | --------------------- | ------- | --------- | | Reads/day | 100,000 | Unlimited | | Writes/day | 1,000 | Unlimited | | Storage | 1 GB | Unlimited | | Namespaces | 1,000 | 1,000 | | Ops/Worker invocation | 1,000 | 1,000 |

| Parameter | Limit | | --------------------- | ----------- | | Key size | 512 bytes | | Value size | 25 MiB | | Metadata size | 1,024 bytes | | Keys per list() | 1,000 | | Keys per multi-get | 100 | | Write rate (same key) | 1/sec | | Minimum TTL | 60 sec |


Pricing

Free plan (per day):

  • 100,000 reads
  • 1,000 writes/deletes/lists
  • 1 GB storage

Paid plan (per month): | Metric | Included | Overage | |--------|----------|---------| | Reads | 10 million | $0.50/million | | Writes | 1 million | $5.00/million | | Deletes | 1 million | $5.00/million | | Lists | 1 million | $5.00/million | | Storage | 1 GB | $0.50/GB |

No egress fees. Dashboard/Wrangler queries are billable.

See pricing.md for optimization tips.


Best Practices

Use prefixes for organization

// Good: prefixed keys
await env.MY_KV.put("user:123:profile", data);
await env.MY_KV.put("user:123:settings", data);
await env.MY_KV.list({ prefix: "user:123:" });

// Bad: flat keys
await env.MY_KV.put("user_123_profile", data);

Store small values in metadata

// Avoid list() + get() for each key
await env.MY_KV.put(key, "", { metadata: { value: smallValue } });

Coalesce related data

// Instead of many small keys
await env.MY_KV.put(
  "user:123:settings",
  JSON.stringify({
    theme: "dark",
    language: "en",
    notifications: true,
  })
);

Handle missing keys

const value = await env.MY_KV.get("key", "json");
if (value === null) {
  // Key doesn't exist or expired
}

Local Development

# Uses local KV by default
wrangler dev

# Use remote/production KV
wrangler dev --remote

Or set "remote": true in binding config.


Prohibitions

  • ❌ Do not write to same key more than 1/sec
  • ❌ Do not rely on immediate consistency after writes
  • ❌ Do not use KV for atomic counters (use Durable Objects)
  • ❌ Do not exceed 25 MiB value size
  • ❌ Do not use bulk write via Workers binding (use REST API)

References

Links

Related Skills

  • cloudflare-workers — Worker development
  • cloudflare-pages - Pages Functions with KV
  • cloudflare-durable-objects - Strong consistency alternative
  • cloudflare-d1 — D1 SQL database operations