Cloudflare Workers KV
Status: Production Ready ✅ Last Updated: 2025-10-21 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: wrangler@4.43.0, @cloudflare/workers-types@4.20251014.0
Quick Start (5 Minutes)
1. Create KV Namespace
# 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>"
For development (preview) namespace:
npx wrangler kv namespace create MY_NAMESPACE --preview
# Output:
# [[kv_namespaces]]
# binding = "MY_NAMESPACE"
# preview_id = "<UUID>"
2. Configure Bindings
Add to your 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)
}
]
}
Or use 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
CRITICAL:
bindingis how you access the namespace in code (env.MY_NAMESPACE)idis the production namespace UUIDpreview_idis for local dev (optional, separate namespace)- Never commit real namespace IDs to public repos - use environment variables or secrets
3. Write Your First Key-Value Pair
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. Test Locally
# 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"}
Complete Workers KV API
1. Read Operations
get() - Read Single Key
// 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 (Bulk)
// 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() - Read with Metadata
// 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);
}
Type Options:
text(default) - Returnsstringjson- Parses JSON, returnsobjectarrayBuffer- ReturnsArrayBufferstream- ReturnsReadableStream
Note: Bulk read with get(keys[]) only supports text and json types. For arrayBuffer or stream, use individual get() calls with Promise.all().
2. Write Operations
put() - Write Key-Value Pair
// 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() },
});
CRITICAL Limits:
- Key size: Maximum 512 bytes
- Value size: Maximum 25 MiB
- Metadata size: Maximum 1024 bytes (JSON serialized)
- Write rate: Maximum 1 write per second per key
- Expiration minimum: 60 seconds (both TTL and absolute)
Rate Limit Handling:
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 Operations
list() - List Keys
// 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);
List Response Format:
{
keys: [
{
name: "user:123",
expiration: 12345