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
-
binding.md — Binding configuration
-
api.md — Workers API reference
-
bulk.md — Bulk operations
-
pricing.md — Billing and optimization
Links
-
Documentation
-
Changelog
Related Skills
-
cloudflare-workers — Worker development
-
cloudflare-pages
-
Pages Functions with KV
-
cloudflare-durable-objects
-
Strong consistency alternative
-
cloudflare-d1 — D1 SQL database operations