
Cloudflare Kv
- 51 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Helps with ai & agent building tasks during AI-assisted development.
About
cloudflare-kv is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- cloudflare-kv
- AI & Agent Building
- AI-coding skill
Cloudflare Kv by the numbers
- 51 all-time installs (skills.sh)
- Ranked #7,118 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill cloudflare-kvAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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>" # optionalCRITICAL:
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: 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
}IMPORTANT:
- Keys are always returned in lexicographically sorted order (UTF-8)
- Always check `list_complete`, not
keys.length === 0 - Empty
keysarray doesn't mean no more data (expired/deleted keys create "tombstones") - When paginating with
prefix, you must pass the sameprefixwith eachcursorrequest
---
4. Delete Operations
delete() - Delete Key
// 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)Bulk Delete via REST API:
The Workers binding doesn't support bulk delete, but you can use the REST API (via wrangler or direct API calls):
# Using wrangler CLI
npx wrangler kv bulk delete --namespace-id=<UUID> keys.json
# keys.json format:
# ["key1", "key2", "key3"]REST API Limit: Up to 10,000 keys per bulk delete request.
---
Advanced Patterns & Best Practices
1. Caching Pattern with 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 Guidelines:
- Minimum: 60 seconds
- Default: 60 seconds
- Maximum:
Number.MAX_SAFE_INTEGER - Use case: Frequently read, infrequently updated data
- Trade-off: Higher cacheTtl = faster reads but slower update propagation
---
2. Metadata Optimization Pattern
Store small values in metadata to avoid separate get() calls:
// ❌ 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!
});When to Use:
- ✅ Values fit in 1024 bytes
- ✅ You frequently use
list()operations - ✅ You need to filter/process many keys
- ❌ Don't use for large values (use regular value storage)
---
3. Key Coalescing for Performance
Combine related cold keys with hot keys:
// ❌ Bad: Many individual keys (some hot, some cold)
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', 'john@example.com');
await kv.put('user:123:age', '30');
// ✅ Good: Coalesce into single hot key
await kv.put('user:123', JSON.stringify({
name: 'John',
email: 'john@example.com',
age: 30,
}));
// Single read gets everything
const user = await kv.get<User>('user:123', { type: 'json' });Advantages:
- Cold keys benefit from hot key caching
- Fewer operations = better performance
- Single cache entry instead of multiple
Disadvantages:
- Can't update individual fields easily (requires read-modify-write)
- Large coalesced values may hit memory limits
- Concurrent updates need locking mechanism
---
4. Pagination Helper
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. Feature Flags Pattern
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 });
});---
Understanding Eventual Consistency
KV is eventually consistent across Cloudflare's global network:
How It Works:
1. Writes are immediately visible in the same location 2. Other locations see the update within ~60 seconds (or your cacheTtl value) 3. Cached reads may return stale data during propagation
Implications:
// 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" ✅Best Practices:
✅ Use KV for:
- Read-heavy workloads (100:1 read/write ratio)
- Data that doesn't require immediate global consistency
- Configuration, feature flags, caching
- User preferences, session data
❌ Don't use KV for:
- Financial transactions requiring atomic operations
- Data requiring strong consistency
- High-frequency writes to same key (>1/second)
- Critical data where stale reads are unacceptable
If you need strong consistency, use [Durable Objects](https://developers.cloudflare.com/durable-objects/).
---
Wrangler CLI Operations
Create Namespace
# Production namespace
npx wrangler kv namespace create MY_NAMESPACE
# Preview/development namespace
npx wrangler kv namespace create MY_NAMESPACE --previewList Namespaces
npx wrangler kv namespace listWrite Key-Value Pairs
# 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=3600Read Key-Value Pairs
# 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.pngList Keys
# 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 Keys
# Delete single key
npx wrangler kv key delete --binding=MY_NAMESPACE "my-key"Bulk Operations
# 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"]---
Limits & Quotas
| Feature | Free Plan | Paid Plan |
|---|---|---|
| Reads per day | 100,000 | Unlimited |
| Writes per day (different keys) | 1,000 | Unlimited |
| Writes per key per second | 1 | 1 |
| Operations per Worker invocation | 1,000 | 1,000 |
| Namespaces per account | 1,000 | 1,000 |
| Storage per account | 1 GB | Unlimited |
| Storage per namespace | 1 GB | Unlimited |
| Keys per namespace | Unlimited | Unlimited |
| Key size | 512 bytes | 512 bytes |
| Metadata size | 1024 bytes | 1024 bytes |
| Value size | 25 MiB | 25 MiB |
| Minimum cacheTtl | 60 seconds | 60 seconds |
| Maximum cacheTtl | Number.MAX_SAFE_INTEGER | Number.MAX_SAFE_INTEGER |
Important Notes:
- 1 write/second per key: Concurrent writes to the same key cause 429 errors
- 1000 operations per invocation: Bulk operations count as 1 operation
- Bulk reads (reading multiple keys) count as a single operation
- REST API is subject to Cloudflare API rate limits
---
TypeScript Types
// 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;
}---
Error Handling
Common Errors
1. Rate Limit (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. Value Too Large
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. Metadata Too Large
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. Invalid CacheTtl
// ❌ Too low
await env.MY_KV.get('key', { cacheTtl: 30 }); // Error: minimum is 60
// ✅ Correct
await env.MY_KV.get('key', { cacheTtl: 60 });---
Always Do ✅
1. Use bulk operations when reading multiple keys (counts as 1 operation) 2. Set cacheTtl for frequently-read, infrequently-updated data 3. Store small values in metadata when using list() frequently 4. Check `list_complete` when paginating, not keys.length === 0 5. Use retry logic with exponential backoff for write operations 6. Validate sizes before writing (key 512 bytes, value 25 MiB, metadata 1 KB) 7. Use preview namespaces for local development 8. Set appropriate TTLs for cache invalidation (minimum 60 seconds) 9. Coalesce related keys for better caching performance 10. Use KV for read-heavy workloads (100:1 read/write ratio ideal)
---
Never Do ❌
1. Never write to same key >1/second - Causes 429 rate limit errors 2. Never assume immediate global consistency - Takes ~60 seconds to propagate 3. Never use KV for atomic operations - Use Durable Objects instead 4. Never set cacheTtl <60 seconds - Will fail 5. Never commit namespace IDs to public repos - Use environment variables 6. Never exceed 1000 operations per invocation - Use bulk operations 7. Never rely on write order - Eventual consistency means no guarantees 8. Never store sensitive data without encryption - KV is not encrypted at rest by default 9. Never use KV for high-frequency writes - Not designed for write-heavy workloads 10. Never forget to handle null values - get() returns null if key doesn't exist
---
Troubleshooting
Issue: "429 Too Many Requests" on writes
Cause: Writing to same key more than once per second
Solution:
// ❌ 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));---
Issue: Stale reads after write
Cause: Eventual consistency - writes take up to 60 seconds to propagate globally
Solution:
// Accept that reads may be stale for up to 60 seconds
// OR use Durable Objects for strong consistency
// OR implement application-level cache invalidation---
Issue: "Operations limit exceeded"
Cause: More than 1000 KV operations in single Worker invocation
Solution:
// ❌ 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);---
Issue: List returns empty but cursor exists
Cause: Recently deleted/expired keys create "tombstones" in the list
Solution:
// 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);---
Production Checklist
Before deploying to production:
- [ ] Environment-specific namespaces configured (
idvspreview_id) - [ ] Namespace IDs stored in environment variables (not hardcoded)
- [ ] Rate limit retry logic implemented for writes
- [ ] Appropriate
cacheTtlvalues set for reads - [ ] Metadata sizes validated (<1024 bytes)
- [ ] Value sizes validated (<25 MiB)
- [ ] Key sizes validated (<512 bytes)
- [ ] Bulk operations used where possible
- [ ] Pagination implemented correctly for
list() - [ ] Error handling for null values
- [ ] Monitoring/alerting for rate limits
- [ ] Documentation for eventual consistency behavior
---
Related Documentation
---
Last Updated: 2025-10-21 Version: 1.0.0 Maintainer: Jeremy Dawes | jeremy@jezweb.net
{
"name": "cloudflare-kv",
"description": "Store key-value data globally with Cloudflare KVs edge network. Use when: caching API responses, storing configuration, managing user preferences, handling TTL expiration, or troubleshooting KV_ERROR, 429 rate limits, eventual consistency, or cacheTtl errors.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"keywords": []
}
Cloudflare Workers KV
Complete knowledge domain for Cloudflare Workers KV - global, low-latency key-value storage on Cloudflare's edge network.
---
Auto-Trigger Keywords
Primary Keywords
- kv storage
- cloudflare kv
- kv namespace
- workers kv
- kv bindings
- kv cache
- kv api
- key value storage
- edge storage
Secondary Keywords
- kv get
- kv put
- kv delete
- kv list
- kv ttl
- kv expiration
- kv metadata
- cache ttl
- kv pagination
- kv prefix
- kv wrangler
- namespace create
- kv operations
- eventually consistent
Error-Based Keywords
- KV_ERROR
- 429 too many requests
- kv rate limit
- kv quota exceeded
- kv write limit
- concurrent writes kv
- kv_put_failed
- metadata too large
- value too large
- key too long
- cacheTtl minimum
Framework Integration Keywords
- kv hono
- kv workers api
- kv cloudflare workers
- wrangler kv
- kv bindings workers
---
What This Skill Does
This skill provides complete Workers KV knowledge including:
- ✅ KV Namespace Management - Create, configure, and bind namespaces
- ✅ CRUD Operations - get(), put(), delete(), list()
- ✅ Bulk Operations - Bulk reads and REST API batch writes
- ✅ Metadata Storage - Store up to 1KB metadata per key
- ✅ TTL & Expiration - Automatic key expiration with TTL or absolute time
- ✅ CacheTtl Optimization - Control edge caching for faster reads
- ✅ List Operations - Pagination with cursor, prefix filtering
- ✅ Performance Patterns - Key coalescing, caching strategies
- ✅ Error Handling - Rate limit retries, eventual consistency handling
- ✅ Development vs Production - Environment-specific namespaces
---
Known Issues Prevented
| Issue | Description | Prevention |
|---|---|---|
| 1 write/sec limit | Concurrent writes to same key cause 429 errors | Document rate limits + retry logic with backoff |
| Eventually consistent | Writes take up to 60s to propagate globally | Set expectations, use cacheTtl appropriately |
| cacheTtl minimum 60s | Setting lower than 60s fails | Always use 60+ seconds for cacheTtl |
| Metadata 1024 byte limit | Exceeding metadata size causes errors | Validate metadata size before put() |
| Value 25MB limit | Large values fail to store | Check size before writing |
| 1000 operations/invocation | Exceeding causes Worker failure | Use bulk operations, batch reads |
---
When to Use This Skill
✅ Use this skill when:
- Storing configuration data or feature flags
- Caching API responses or computed values
- Storing user preferences or session data
- Building read-heavy applications
- Implementing A/B testing configurations
- Storing authentication tokens or JWT data
- Building CDN-like caching layers
- Storing routing tables or redirect maps
- Managing global application state
❌ When NOT to use:
- You need strong consistency (use Durable Objects)
- You need atomic operations (use Durable Objects)
- You need relational data (use cloudflare-d1)
- You need frequent writes to same key (>1/sec)
- You need large file storage (use cloudflare-r2)
- You need vector search (use cloudflare-vectorize)
---
Quick Example
import { Hono } from 'hono';
type Bindings = {
CONFIG: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// Write with expiration
app.post('/config/:key', async (c) => {
const key = c.req.param('key');
const value = await c.req.text();
await c.env.CONFIG.put(key, value, {
expirationTtl: 3600, // Expire in 1 hour
metadata: { updatedAt: Date.now(), updatedBy: 'admin' },
});
return c.json({ success: true, key });
});
// Read with cache optimization
app.get('/config/:key', async (c) => {
const key = c.req.param('key');
const { value, metadata } = await c.env.CONFIG.getWithMetadata(key, {
type: 'json',
cacheTtl: 300, // Cache for 5 minutes at edge
});
if (!value) {
return c.json({ error: 'Not found' }, 404);
}
return c.json({ value, metadata });
});
// List with prefix and pagination
app.get('/config/list/:prefix?', async (c) => {
const prefix = c.req.param('prefix') || '';
const cursor = c.req.query('cursor');
const result = await c.env.CONFIG.list({
prefix,
limit: 100,
cursor: cursor || undefined,
});
return c.json({
keys: result.keys,
hasMore: !result.list_complete,
cursor: result.cursor,
});
});
export default app;---
Token Efficiency
- Manual Setup: 9,000-13,000 tokens
- With This Skill: 3,500-5,000 tokens
- Savings: ~55-60%
---
Files Included
SKILL.md- Complete KV knowledge domaintemplates/wrangler-kv-config.jsonc- KV namespace bindingstemplates/kv-basic-operations.ts- CRUD operations Workertemplates/kv-caching-pattern.ts- Cache optimization patternstemplates/kv-list-pagination.ts- List with cursor paginationtemplates/kv-metadata-pattern.ts- Metadata usage patternsreference/workers-api.md- Complete Workers API referencereference/best-practices.md- Performance & caching strategies
---
Dependencies
- cloudflare-worker-base - For Hono + Vite + Worker setup
- wrangler - For KV namespace management
---
Production Status
✅ Production Ready
This skill is based on:
- Official Cloudflare KV documentation
- Cloudflare Workers SDK examples
- Production-tested patterns
- Latest package versions (verified 2025-10-21)
---
Related Skills
- cloudflare-worker-base - Base Worker setup with Hono
- cloudflare-d1 - Serverless SQLite database
- cloudflare-r2 - Object storage
- cloudflare-workers-ai - AI inference on Workers
---
Last Updated: 2025-10-21 Status: Production Ready ✅ Maintainer: Jeremy Dawes | jeremy@jezweb.net
Cloudflare Workers KV - Best Practices
This document contains production-tested best practices for Cloudflare Workers KV.
---
Table of Contents
1. Performance Optimization 2. Caching Strategies 3. Key Design 4. Metadata Usage 5. Error Handling 6. Security 7. Cost Optimization 8. Monitoring & Debugging
---
Performance Optimization
1. Use Bulk Operations
❌ Bad: Individual reads
const value1 = await kv.get('key1'); // 1 operation
const value2 = await kv.get('key2'); // 1 operation
const value3 = await kv.get('key3'); // 1 operation
// Total: 3 operations✅ Good: Bulk read
const values = await kv.get(['key1', 'key2', 'key3']); // 1 operation
// Total: 1 operationBenefits:
- Counts as 1 operation against the 1000/invocation limit
- Faster execution
- Lower latency
---
2. Use CacheTtl for Frequently-Read Data
❌ Bad: No edge caching
const value = await kv.get('config'); // Fetches from KV every time✅ Good: Edge caching
const value = await kv.get('config', {
cacheTtl: 300, // Cache at edge for 5 minutes
});Guidelines:
- Use
cacheTtlfor data that changes infrequently - Minimum: 60 seconds
- Typical values:
- Configuration: 300-600 seconds (5-10 minutes)
- Static content: 3600+ seconds (1+ hour)
- Frequently changing: 60-120 seconds
Trade-off: Higher cacheTtl = faster reads but slower updates propagate
---
3. Coalesce Related Keys
❌ Bad: Many small keys
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', 'john@example.com');
await kv.put('user:123:age', '30');
// Reading requires 3 operations
const name = await kv.get('user:123:name');
const email = await kv.get('user:123:email');
const age = await kv.get('user:123:age');✅ Good: Coalesced key
await kv.put('user:123', JSON.stringify({
name: 'John',
email: 'john@example.com',
age: 30,
}));
// Reading requires 1 operation
const user = await kv.get<User>('user:123', { type: 'json' });Benefits:
- Fewer operations
- Single cache entry
- Faster reads
When to use:
- Related data that's always accessed together
- Data that doesn't update frequently
- Values stay under 25 MiB total
---
4. Store Small Values in Metadata
❌ Bad: Separate keys for metadata
await kv.put('user:123', 'data');
await kv.put('user:123:status', 'active');
// List requires additional get() calls
const users = await kv.list({ prefix: 'user:' });
for (const key of users.keys) {
const status = await kv.get(`${key.name}:status`); // Extra operation!
}✅ Good: Metadata pattern
await kv.put('user:123', 'data', {
metadata: { status: 'active', plan: 'pro' },
});
// List includes metadata, no extra get() calls!
const users = await kv.list({ prefix: 'user:' });
for (const key of users.keys) {
console.log(key.name, key.metadata.status); // No extra operations
}When to use:
- Values fit in 1024 bytes
- Frequently use
list()operations - Need to filter/process many keys
---
Caching Strategies
1. Cache-Aside Pattern (Read-Through)
async function getCached<T>(
kv: KVNamespace,
key: string,
fetchFn: () => Promise<T>,
ttl = 3600
): Promise<T> {
// Try cache
const cached = await kv.get<T>(key, {
type: 'json',
cacheTtl: 300,
});
if (cached !== null) return cached;
// Cache miss - fetch and store
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl });
return data;
}Use when:
- Data is expensive to compute/fetch
- Read >> Write ratio
- Acceptable to serve slightly stale data
---
2. Write-Through Cache
async function updateCached<T>(
kv: KVNamespace,
key: string,
data: T,
ttl = 3600
): Promise<void> {
// Update database
await database.update(data);
// Update cache immediately
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl });
}Use when:
- Need cache consistency
- Write operations are infrequent
- Cache must always reflect latest data
---
3. Stale-While-Revalidate
async function staleWhileRevalidate<T>(
kv: KVNamespace,
key: string,
fetchFn: () => Promise<T>,
ctx: ExecutionContext,
staleThreshold = 300
): Promise<T> {
const { value, metadata } = await kv.getWithMetadata<T, { timestamp: number }>(
key,
{ type: 'json' }
);
if (value !== null && metadata) {
const age = Date.now() - metadata.timestamp;
// Refresh in background if stale
if (age > staleThreshold * 1000) {
ctx.waitUntil(
(async () => {
const fresh = await fetchFn();
await kv.put(key, JSON.stringify(fresh), {
metadata: { timestamp: Date.now() },
});
})()
);
}
return value;
}
// Cache miss
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), {
metadata: { timestamp: Date.now() },
});
return data;
}Use when:
- Fast response time is critical
- Acceptable to serve slightly stale data
- Background refresh is acceptable
---
Key Design
1. Use Hierarchical Namespaces
✅ Good key patterns:
user:123:profile
user:123:settings
user:123:sessions
session:abc123:data
session:abc123:metadata
cache:api:users:list
cache:api:posts:123
cache:db:query:hash123Benefits:
- Easy to filter with
list({ prefix: 'user:123:' }) - Easy to invalidate groups
- Clear organization
---
2. Use Lexicographic Ordering
Keys are always sorted lexicographically, so design keys to take advantage:
// Date-based keys (ISO format sorts correctly)
'log:2025-10-21:entry1'
'log:2025-10-22:entry1'
// Numeric IDs (zero-padded)
'user:00000001'
'user:00000123'
'user:00001000'
// Priority-based (prefix with number)
'task:1:high-priority'
'task:2:medium-priority'
'task:3:low-priority'---
3. Avoid Key Collisions
❌ Bad:
user:123 // User data
user:123:count // Some counter
user:123 // Different data? Collision!✅ Good:
user:data:123
user:counter:123
user:session:123---
Metadata Usage
1. Track Versions
await kv.put('config', JSON.stringify(data), {
metadata: {
version: 2,
updatedAt: Date.now(),
updatedBy: 'admin',
},
});---
2. Audit Trails
await kv.put(key, value, {
metadata: {
createdAt: Date.now(),
createdBy: userId,
accessCount: 0,
},
});---
3. Feature Flags
// Store flags in metadata for fast list() access
await kv.put(`flag:${name}`, JSON.stringify(config), {
metadata: {
enabled: true,
rolloutPercentage: 50,
},
});
// List all flags without additional get() calls
const flags = await kv.list({ prefix: 'flag:' });---
Error Handling
1. Handle Rate Limits (429)
async function putWithRetry(
kv: KVNamespace,
key: string,
value: string,
maxAttempts = 5
): Promise<void> {
let attempts = 0;
let delay = 1000;
while (attempts < maxAttempts) {
try {
await kv.put(key, value);
return;
} 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');
}
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
} else {
throw error;
}
}
}
}---
2. Handle Null Values
// ❌ Bad
const value = await kv.get('key');
console.log(value.toUpperCase()); // Error if key doesn't exist
// ✅ Good
const value = await kv.get('key');
if (value !== null) {
console.log(value.toUpperCase());
}
// ✅ Good: Default value
const value = await kv.get('key') ?? 'default';---
3. Validate Input Sizes
function validateKVInput(key: string, value: string, metadata?: any): void {
// Key size
if (new TextEncoder().encode(key).length > 512) {
throw new Error('Key exceeds 512 bytes');
}
// Value size
if (new TextEncoder().encode(value).length > 25 * 1024 * 1024) {
throw new Error('Value exceeds 25 MiB');
}
// Metadata size
if (metadata) {
const serialized = JSON.stringify(metadata);
if (new TextEncoder().encode(serialized).length > 1024) {
throw new Error('Metadata exceeds 1024 bytes');
}
}
}---
Security
1. Never Commit Namespace IDs
❌ Bad:
{
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "abc123def456..." // Hardcoded!
}
]
}✅ Good:
{
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "${KV_NAMESPACE_ID}" // Environment variable
}
]
}---
2. Encrypt Sensitive Data
// Encrypt before storing
const encrypted = await encrypt(sensitiveData, encryptionKey);
await kv.put('sensitive:123', encrypted);
// Decrypt after reading
const encrypted = await kv.get('sensitive:123');
const decrypted = await decrypt(encrypted, encryptionKey);---
3. Use Separate Namespaces for Environments
{
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "production-namespace-id",
"preview_id": "development-namespace-id"
}
]
}---
Cost Optimization
1. Minimize Write Operations (Free Tier)
Free tier limits:
- 1,000 writes per day
- 100,000 reads per day
Strategies:
- Batch writes when possible
- Use longer TTLs to reduce rewrites
- Cache data in memory if accessed frequently within same invocation
---
2. Use Metadata Instead of Separate Keys
❌ Expensive: 3 writes, 3 reads
await kv.put('user:123:status', 'active');
await kv.put('user:123:plan', 'pro');
await kv.put('user:123:updated', Date.now().toString());✅ Cheaper: 1 write, 1 read
await kv.put('user:123', '', {
metadata: { status: 'active', plan: 'pro', updated: Date.now() },
});---
3. Set Appropriate TTLs
Longer TTLs = fewer rewrites = lower costs
// ❌ Expensive: Rewrites every minute
await kv.put('cache:data', data, { expirationTtl: 60 });
// ✅ Better: Rewrites every hour
await kv.put('cache:data', data, { expirationTtl: 3600 });---
Monitoring & Debugging
1. Track Cache Hit Rates
let stats = { hits: 0, misses: 0 };
async function getCached<T>(kv: KVNamespace, key: string): Promise<T | null> {
const value = await kv.get<T>(key, { type: 'json' });
if (value !== null) {
stats.hits++;
} else {
stats.misses++;
}
return value;
}
// View stats
app.get('/stats', (c) => {
const total = stats.hits + stats.misses;
const hitRate = total > 0 ? (stats.hits / total) * 100 : 0;
return c.json({
hits: stats.hits,
misses: stats.misses,
hitRate: `${hitRate.toFixed(2)}%`,
});
});---
2. Log KV Operations
async function loggedGet<T>(
kv: KVNamespace,
key: string
): Promise<T | null> {
const start = Date.now();
const value = await kv.get<T>(key, { type: 'json' });
const duration = Date.now() - start;
console.log({
operation: 'get',
key,
found: value !== null,
duration,
});
return value;
}---
3. Use Namespace Prefixes for Testing
const namespace = env.ENVIRONMENT === 'production' ? 'prod' : 'test';
await kv.put(`${namespace}:user:123`, data);
// Cleanup test data
if (env.ENVIRONMENT === 'test') {
// Delete all test: keys
let cursor: string | undefined;
do {
const result = await kv.list({ prefix: 'test:', cursor });
await Promise.all(result.keys.map(k => kv.delete(k.name)));
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
}---
Production Checklist
Before deploying to production:
- [ ] Environment-specific namespaces configured
- [ ] Namespace IDs stored in environment variables
- [ ] Rate limit retry logic implemented
- [ ] Appropriate
cacheTtlvalues set - [ ] Input validation for key/value/metadata sizes
- [ ] Bulk operations used where possible
- [ ] Pagination implemented correctly for
list() - [ ] Error handling for null values
- [ ] Monitoring/alerting for rate limits
- [ ] Documentation for eventual consistency behavior
- [ ] Security review for sensitive data
- [ ] Cost analysis for expected usage
---
Common Patterns
1. Session Management
// Store session
await kv.put(`session:${sessionId}`, JSON.stringify(sessionData), {
expirationTtl: 3600, // 1 hour
metadata: { userId, createdAt: Date.now() },
});
// Read session
const session = await kv.get<SessionData>(`session:${sessionId}`, {
type: 'json',
cacheTtl: 60, // Cache for 1 minute
});---
2. API Response Caching
const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`;
let response = await kv.get<ApiResponse>(cacheKey, {
type: 'json',
cacheTtl: 300,
});
if (!response) {
response = await fetchFromAPI(endpoint, params);
await kv.put(cacheKey, JSON.stringify(response), {
expirationTtl: 600,
});
}
return response;---
3. Configuration Management
// Update config
await kv.put('config:app', JSON.stringify(config), {
metadata: {
version: 2,
updatedAt: Date.now(),
updatedBy: adminId,
},
});
// Read config (with long cache)
const config = await kv.get<AppConfig>('config:app', {
type: 'json',
cacheTtl: 3600, // Cache for 1 hour
});---
References
Cloudflare Workers KV - Complete API Reference
This document provides the complete Workers KV API reference based on official Cloudflare documentation.
---
KVNamespace Interface
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(keys: string[]): 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<ExpectedValue = unknown, Metadata = unknown>(key: string, type: "json"): Promise<KVGetWithMetadataResult<ExpectedValue, Metadata>>;
getWithMetadata<Metadata = unknown>(keys: string[]): Promise<Map<string, KVGetWithMetadataResult<string, 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>>;
}---
Read Operations
get() - Single Key
Read a single key-value pair.
Signature:
get(key: string, options?: KVGetOptions): Promise<T | null>Parameters:
key(string, required) - The key to readoptions(object, optional):type- Return type:"text"(default),"json","arrayBuffer","stream"cacheTtl(number) - Edge cache duration in seconds (minimum: 60)
Returns:
Promise<T | null>- Value ornullif key doesn't exist
Examples:
// Text (default)
const value = await env.MY_KV.get('my-key');
// JSON
const data = await env.MY_KV.get<MyType>('my-key', { type: 'json' });
// With cache optimization
const value = await env.MY_KV.get('my-key', {
type: 'text',
cacheTtl: 300, // Cache for 5 minutes
});
// ArrayBuffer
const buffer = await env.MY_KV.get('binary-key', { type: 'arrayBuffer' });
// Stream (for large values)
const stream = await env.MY_KV.get('large-file', { type: 'stream' });---
get() - Multiple Keys (Bulk)
Read multiple keys in a single operation.
Signature:
get(keys: string[], type?: 'text' | 'json'): Promise<Map<string, T | null>>Parameters:
keys(string[], required) - Array of keys to readtype(optional) - Return type:"text"(default) or"json"
Returns:
Promise<Map<string, T | null>>- Map of key-value pairs
Important:
- Counts as 1 operation regardless of number of keys
- Only supports
textandjsontypes (notarrayBufferorstream) - For binary/stream types, use individual
get()calls withPromise.all()
Examples:
// Read multiple keys
const keys = ['key1', 'key2', 'key3'];
const values = await env.MY_KV.get(keys);
// Access values
const value1 = values.get('key1');
const value2 = values.get('key2');
// Convert to object
const obj = Object.fromEntries(values);
// Read as JSON
const values = await env.MY_KV.get<MyType>(keys, 'json');---
getWithMetadata() - Single Key
Read key-value pair with metadata.
Signature:
getWithMetadata<Value, Metadata>(
key: string,
options?: KVGetOptions
): Promise<KVGetWithMetadataResult<Value, Metadata>>Parameters:
- Same as
get()
Returns:
{
value: Value | null,
metadata: Metadata | null
}Examples:
// Get with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('my-key');
// Get as JSON with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata<MyType>('my-key', {
type: 'json',
cacheTtl: 300,
});
if (value !== null) {
console.log('Value:', value);
console.log('Metadata:', metadata);
}---
getWithMetadata() - Multiple Keys (Bulk)
Read multiple keys with metadata.
Signature:
getWithMetadata<Metadata>(
keys: string[],
type?: 'text' | 'json'
): Promise<Map<string, KVGetWithMetadataResult<T, Metadata>>>Examples:
const keys = ['key1', 'key2'];
const results = await env.MY_KV.getWithMetadata(keys);
for (const [key, data] of results) {
console.log(key, data.value, data.metadata);
}---
Write Operations
put() - Write Key-Value Pair
Write or update a key-value pair.
Signature:
put(
key: string,
value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
options?: KVPutOptions
): Promise<void>Parameters:
key(string, required) - Maximum 512 bytesvalue(required) - Maximum 25 MiBoptions(object, optional):expiration(number) - Absolute expiration time (seconds since epoch)expirationTtl(number) - TTL in seconds from now (minimum: 60)metadata(any) - JSON-serializable metadata (maximum: 1024 bytes)
Returns:
Promise<void>
Examples:
// Simple write
await env.MY_KV.put('key', 'value');
// Write JSON
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
// Write with TTL
await env.MY_KV.put('session', sessionData, {
expirationTtl: 3600, // Expire in 1 hour
});
// Write with absolute expiration
const expirationTime = Math.floor(Date.now() / 1000) + 86400; // 24 hours
await env.MY_KV.put('token', tokenValue, {
expiration: expirationTime,
});
// Write with metadata
await env.MY_KV.put('config', configData, {
metadata: {
updatedAt: Date.now(),
updatedBy: 'admin',
version: 2,
},
});
// Write with everything
await env.MY_KV.put('key', 'value', {
expirationTtl: 600,
metadata: { source: 'api' },
});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
---
Delete Operations
delete() - Delete Key
Delete a key-value pair.
Signature:
delete(key: string): Promise<void>Parameters:
key(string, required) - Key to delete
Returns:
Promise<void>- Always succeeds, even if key doesn't exist
Examples:
// Delete single key
await env.MY_KV.delete('my-key');
// Delete multiple keys
const keys = ['key1', 'key2', 'key3'];
await Promise.all(keys.map(key => env.MY_KV.delete(key)));Note: For bulk delete of >10,000 keys, use the REST API.
---
List Operations
list() - List Keys
List keys in the namespace.
Signature:
list<Metadata>(options?: KVListOptions): Promise<KVListResult<Metadata>>Parameters:
interface KVListOptions {
prefix?: string; // Filter keys by prefix
limit?: number; // Max keys to return (default: 1000, max: 1000)
cursor?: string; // Pagination cursor
}Returns:
interface KVListResult<Metadata> {
keys: {
name: string;
expiration?: number; // Seconds since epoch
metadata?: Metadata;
}[];
list_complete: boolean; // true if no more keys
cursor?: string; // Use for next page
}Examples:
// List all keys (up to 1000)
const result = await env.MY_KV.list();
// List with prefix
const users = await env.MY_KV.list({ prefix: 'user:' });
// List with limit
const recent = await env.MY_KV.list({ limit: 100 });
// Pagination
let cursor: string | undefined;
do {
const result = await env.MY_KV.list({ cursor });
// Process keys
console.log(result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);Important:
- Keys are always sorted lexicographically (UTF-8)
- Always check `list_complete`, not
keys.length === 0 - Empty
keysarray doesn't mean no more data (tombstones exist) - When paginating with
prefix, passprefixwith each cursor request
---
Type Definitions
KVGetOptions
interface KVGetOptions<Type> {
type: Type; // "text" | "json" | "arrayBuffer" | "stream"
cacheTtl?: number; // Edge cache duration (minimum: 60 seconds)
}KVPutOptions
interface KVPutOptions {
expiration?: number; // Seconds since epoch
expirationTtl?: number; // Seconds from now (minimum: 60)
metadata?: any; // Max 1024 bytes serialized
}KVGetWithMetadataResult
interface KVGetWithMetadataResult<Value, Metadata> {
value: Value | null;
metadata: Metadata | null;
}KVListOptions
interface KVListOptions {
prefix?: string;
limit?: number; // Default: 1000, max: 1000
cursor?: string;
}KVListResult
interface KVListResult<Metadata = unknown> {
keys: {
name: string;
expiration?: number;
metadata?: Metadata;
}[];
list_complete: boolean;
cursor?: string;
}---
Limits
| Feature | Limit |
|---|---|
| Key size | 512 bytes |
| Value size | 25 MiB |
| Metadata size | 1024 bytes (JSON) |
| Writes per key per second | 1 |
| Operations per Worker invocation | 1,000 |
| List limit | 1,000 keys |
| Minimum cacheTtl | 60 seconds |
| Minimum expiration | 60 seconds |
| Namespaces per account (Free) | 1,000 |
| Namespaces per account (Paid) | 1,000 |
| Storage per account (Free) | 1 GB |
| Storage per account (Paid) | Unlimited |
| Read operations per day (Free) | 100,000 |
| Read operations per day (Paid) | Unlimited |
| Write operations per day (Free) | 1,000 |
| Write operations per day (Paid) | Unlimited |
---
Consistency Model
Eventually Consistent
- Writes are immediately visible in the same location
- Writes take up to 60 seconds to propagate globally
- Cached reads may return stale data during propagation
Implications
// Tokyo datacenter
await env.KV.put('counter', '1');
const value1 = await env.KV.get('counter'); // "1" ✅
// London datacenter (within 60 seconds)
const value2 = await env.KV.get('counter'); // Might be old value ⚠️
// After 60+ seconds globally
const value3 = await env.KV.get('counter'); // "1" ✅For strong consistency, use [Durable Objects](https://developers.cloudflare.com/durable-objects/).
---
Error Handling
Common Errors
1. 429 Too Many Requests
- Cause: >1 write/second to same key
- Solution: Implement retry with exponential backoff
2. Value too large
- Cause: Value >25 MiB
- Solution: Validate size before writing
3. Metadata too large
- Cause: Metadata >1024 bytes serialized
- Solution: Validate JSON size before writing
4. Invalid cacheTtl
- Cause: cacheTtl <60 seconds
- Solution: Use minimum 60 seconds
5. Operations limit exceeded
- Cause: >1000 KV operations in Worker invocation
- Solution: Use bulk operations
---
References
/**
* Cloudflare Workers KV - Basic CRUD Operations
*
* This template demonstrates all basic KV operations:
* - Create (PUT)
* - Read (GET)
* - Update (PUT)
* - Delete (DELETE)
* - List keys
* - Metadata handling
* - TTL/Expiration
* - Error handling
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// CREATE / UPDATE - Write key-value pairs
// ============================================================================
// Simple write
app.put('/kv/:key', async (c) => {
const key = c.req.param('key');
const value = await c.req.text();
try {
await c.env.MY_KV.put(key, value);
return c.json({
success: true,
message: `Key "${key}" created/updated`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Write with TTL expiration
app.put('/kv/:key/ttl/:seconds', async (c) => {
const key = c.req.param('key');
const ttl = parseInt(c.req.param('seconds'), 10);
const value = await c.req.text();
// Validate TTL (minimum 60 seconds)
if (ttl < 60) {
return c.json(
{
success: false,
error: 'TTL must be at least 60 seconds',
},
400
);
}
try {
await c.env.MY_KV.put(key, value, {
expirationTtl: ttl,
});
return c.json({
success: true,
message: `Key "${key}" will expire in ${ttl} seconds`,
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Write with metadata
app.put('/kv/:key/metadata', async (c) => {
const key = c.req.param('key');
const body = await c.req.json<{ value: string; metadata: any }>();
// Validate metadata size (max 1024 bytes serialized)
const metadataJson = JSON.stringify(body.metadata);
if (metadataJson.length > 1024) {
return c.json(
{
success: false,
error: `Metadata too large: ${metadataJson.length} bytes (max 1024)`,
},
400
);
}
try {
await c.env.MY_KV.put(key, body.value, {
metadata: body.metadata,
});
return c.json({
success: true,
message: `Key "${key}" created with metadata`,
metadata: body.metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Write JSON data
app.post('/kv/json/:key', async (c) => {
const key = c.req.param('key');
const data = await c.req.json();
try {
await c.env.MY_KV.put(key, JSON.stringify(data));
return c.json({
success: true,
message: `JSON data stored at key "${key}"`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// READ - Get key-value pairs
// ============================================================================
// Simple read (text)
app.get('/kv/:key', async (c) => {
const key = c.req.param('key');
try {
const value = await c.env.MY_KV.get(key);
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read JSON data
app.get('/kv/json/:key', async (c) => {
const key = c.req.param('key');
try {
const value = await c.env.MY_KV.get(key, { type: 'json' });
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with metadata
app.get('/kv/:key/metadata', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata(key);
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with cache optimization
app.get('/kv/:key/cached', async (c) => {
const key = c.req.param('key');
const cacheTtl = parseInt(c.req.query('cacheTtl') || '300', 10);
// Validate cacheTtl (minimum 60 seconds)
if (cacheTtl < 60) {
return c.json(
{
success: false,
error: 'cacheTtl must be at least 60 seconds',
},
400
);
}
try {
const value = await c.env.MY_KV.get(key, {
type: 'text',
cacheTtl,
});
if (value === null) {
return c.json(
{
success: false,
error: `Key "${key}" not found`,
},
404
);
}
return c.json({
success: true,
key,
value,
cached: true,
cacheTtl,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Bulk read (multiple keys)
app.post('/kv/bulk/get', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
if (!Array.isArray(keys) || keys.length === 0) {
return c.json(
{
success: false,
error: 'keys must be a non-empty array',
},
400
);
}
try {
// Bulk read counts as 1 operation!
const values = await c.env.MY_KV.get(keys);
// Convert Map to object
const result: Record<string, string | null> = {};
for (const [key, value] of values) {
result[key] = value;
}
return c.json({
success: true,
count: keys.length,
values: result,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// LIST - List keys with pagination
// ============================================================================
// List all keys (with pagination)
app.get('/kv/list', async (c) => {
const prefix = c.req.query('prefix') || '';
const cursor = c.req.query('cursor');
const limit = parseInt(c.req.query('limit') || '1000', 10);
try {
const result = await c.env.MY_KV.list({
prefix,
limit,
cursor: cursor || undefined,
});
return c.json({
success: true,
keys: result.keys,
count: result.keys.length,
hasMore: !result.list_complete,
cursor: result.cursor,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all keys with prefix (fully paginated)
app.get('/kv/list/all/:prefix', async (c) => {
const prefix = c.req.param('prefix');
let cursor: string | undefined;
const allKeys: any[] = [];
try {
// Paginate through all keys
do {
const result = await c.env.MY_KV.list({
prefix,
cursor,
});
allKeys.push(...result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
prefix,
keys: allKeys,
totalCount: allKeys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// DELETE - Delete key-value pairs
// ============================================================================
// Delete single key
app.delete('/kv/:key', async (c) => {
const key = c.req.param('key');
try {
// Delete always succeeds, even if key doesn't exist
await c.env.MY_KV.delete(key);
return c.json({
success: true,
message: `Key "${key}" deleted`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Delete multiple keys
app.post('/kv/bulk/delete', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
if (!Array.isArray(keys) || keys.length === 0) {
return c.json(
{
success: false,
error: 'keys must be a non-empty array',
},
400
);
}
try {
// Delete all keys in parallel
await Promise.all(keys.map((key) => c.env.MY_KV.delete(key)));
return c.json({
success: true,
message: `${keys.length} keys deleted`,
count: keys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// UTILITY - Helper endpoints
// ============================================================================
// Check if key exists
app.get('/kv/:key/exists', async (c) => {
const key = c.req.param('key');
try {
const value = await c.env.MY_KV.get(key);
return c.json({
exists: value !== null,
key,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get namespace stats
app.get('/kv/stats', async (c) => {
try {
const result = await c.env.MY_KV.list();
let totalKeys = result.keys.length;
let cursor = result.cursor;
// Count all keys (with pagination)
while (!result.list_complete && cursor) {
const nextResult = await c.env.MY_KV.list({ cursor });
totalKeys += nextResult.keys.length;
cursor = nextResult.cursor;
}
return c.json({
success: true,
totalKeys,
sample: result.keys.slice(0, 10), // First 10 keys
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;
/**
* Cloudflare Workers KV - Caching Pattern
*
* This template demonstrates optimal caching patterns with KV:
* - Cache-aside pattern (read-through cache)
* - Write-through cache
* - CacheTtl optimization for edge caching
* - Stale-while-revalidate pattern
* - Cache invalidation
*/
import { Hono } from 'hono';
type Bindings = {
CACHE: KVNamespace;
DB: D1Database; // Example: database for cache misses
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Cache-Aside Pattern (Read-Through Cache)
// ============================================================================
/**
* Generic cache-aside helper
*
* 1. Try to read from cache
* 2. On miss, fetch from source
* 3. Store in cache
* 4. Return data
*/
async function getCached<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
options: {
ttl?: number; // KV expiration (default: 3600)
cacheTtl?: number; // Edge cache TTL (default: 300)
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600; // 1 hour default
const cacheTtl = options.cacheTtl ?? 300; // 5 minutes default
// Try cache first (with edge caching)
const cached = await kv.get<T>(cacheKey, {
type: 'json',
cacheTtl: Math.max(cacheTtl, 60), // Minimum 60 seconds
});
if (cached !== null) {
return cached;
}
// Cache miss - fetch from source
const data = await fetchFn();
// Store in cache (fire-and-forget)
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: Math.max(ttl, 60), // Minimum 60 seconds
});
return data;
}
// Example: Cache API response
app.get('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
try {
const user = await getCached(
c.env.CACHE,
cacheKey,
async () => {
// Simulate database fetch
const result = await c.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
)
.bind(userId)
.first();
if (!result) {
throw new Error('User not found');
}
return result;
},
{
ttl: 3600, // Cache in KV for 1 hour
cacheTtl: 300, // Cache at edge for 5 minutes
}
);
return c.json({
success: true,
user,
cached: true,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Write-Through Cache Pattern
// ============================================================================
/**
* Write-through cache: Update cache when data changes
*/
app.put('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const userData = await c.req.json();
const cacheKey = `user:${userId}`;
try {
// Update database
await c.env.DB.prepare(
'UPDATE users SET name = ?, email = ? WHERE id = ?'
)
.bind(userData.name, userData.email, userId)
.run();
// Update cache immediately
await c.env.CACHE.put(cacheKey, JSON.stringify(userData), {
expirationTtl: 3600,
});
return c.json({
success: true,
message: 'User updated and cache refreshed',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Invalidation
// ============================================================================
/**
* Invalidate cache when data changes
*/
app.delete('/api/user/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
try {
// Delete from database
await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId).run();
// Invalidate cache
await c.env.CACHE.delete(cacheKey);
return c.json({
success: true,
message: 'User deleted and cache invalidated',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Invalidate multiple cache keys
app.post('/api/cache/invalidate', async (c) => {
const { keys } = await c.req.json<{ keys: string[] }>();
try {
// Delete all cache keys in parallel
await Promise.all(keys.map((key) => c.env.CACHE.delete(key)));
return c.json({
success: true,
message: `${keys.length} cache keys invalidated`,
count: keys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Invalidate by prefix (requires list + delete)
app.post('/api/cache/invalidate/prefix', async (c) => {
const { prefix } = await c.req.json<{ prefix: string }>();
try {
let cursor: string | undefined;
let deletedCount = 0;
// List all keys with prefix and delete them
do {
const result = await c.env.CACHE.list({ prefix, cursor });
// Delete batch in parallel
await Promise.all(result.keys.map((key) => c.env.CACHE.delete(key.name)));
deletedCount += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Cache invalidated for prefix "${prefix}"`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Stale-While-Revalidate Pattern
// ============================================================================
/**
* Return cached data immediately, refresh in background
*/
async function staleWhileRevalidate<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
ctx: ExecutionContext,
options: {
ttl?: number;
staleThreshold?: number; // Refresh if older than this
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600;
const staleThreshold = options.staleThreshold ?? 300; // 5 minutes
// Get cached value with metadata
const { value, metadata } = await kv.getWithMetadata<
T,
{ timestamp: number }
>(cacheKey, { type: 'json' });
// If cached and not too stale, return immediately
if (value !== null && metadata) {
const age = Date.now() - metadata.timestamp;
// If stale, refresh in background
if (age > staleThreshold * 1000) {
ctx.waitUntil(
(async () => {
try {
const fresh = await fetchFn();
await kv.put(cacheKey, JSON.stringify(fresh), {
expirationTtl: ttl,
metadata: { timestamp: Date.now() },
});
} catch (error) {
console.error('Background refresh failed:', error);
}
})()
);
}
return value;
}
// Cache miss - fetch and store
const data = await fetchFn();
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: ttl,
metadata: { timestamp: Date.now() },
});
return data;
}
// Example usage
app.get('/api/stats', async (c) => {
try {
const stats = await staleWhileRevalidate(
c.env.CACHE,
'global:stats',
async () => {
// Expensive computation
const result = await c.env.DB.prepare(
'SELECT COUNT(*) as total FROM users'
).first();
return result;
},
c.executionCtx,
{
ttl: 3600, // Cache for 1 hour
staleThreshold: 300, // Refresh if older than 5 minutes
}
);
return c.json({
success: true,
stats,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Multi-Layer Cache (KV + Memory)
// ============================================================================
/**
* Two-tier cache: In-memory cache + KV cache
* Useful for frequently accessed data within same Worker instance
*/
const memoryCache = new Map<string, { value: any; expires: number }>();
async function getMultiLayerCache<T>(
kv: KVNamespace,
cacheKey: string,
fetchFn: () => Promise<T>,
options: {
ttl?: number;
memoryTtl?: number; // In-memory cache duration
} = {}
): Promise<T> {
const ttl = options.ttl ?? 3600;
const memoryTtl = (options.memoryTtl ?? 60) * 1000; // Convert to ms
// Check memory cache first (fastest)
const memoryCached = memoryCache.get(cacheKey);
if (memoryCached && memoryCached.expires > Date.now()) {
return memoryCached.value;
}
// Check KV cache (fast, global)
const kvCached = await kv.get<T>(cacheKey, {
type: 'json',
cacheTtl: 300,
});
if (kvCached !== null) {
// Store in memory cache
memoryCache.set(cacheKey, {
value: kvCached,
expires: Date.now() + memoryTtl,
});
return kvCached;
}
// Cache miss - fetch from source
const data = await fetchFn();
// Store in both caches
memoryCache.set(cacheKey, {
value: data,
expires: Date.now() + memoryTtl,
});
await kv.put(cacheKey, JSON.stringify(data), {
expirationTtl: ttl,
});
return data;
}
// Example usage
app.get('/api/config', async (c) => {
try {
const config = await getMultiLayerCache(
c.env.CACHE,
'app:config',
async () => {
// Fetch from database or API
return {
theme: 'dark',
features: ['feature1', 'feature2'],
version: '1.0.0',
};
},
{
ttl: 3600, // KV cache: 1 hour
memoryTtl: 60, // Memory cache: 1 minute
}
);
return c.json({
success: true,
config,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Warming
// ============================================================================
/**
* Pre-populate cache with frequently accessed data
*/
app.post('/api/cache/warm', async (c) => {
try {
// Example: Warm cache with top 100 users
const topUsers = await c.env.DB.prepare(
'SELECT * FROM users ORDER BY activity DESC LIMIT 100'
).all();
// Store each user in cache
const promises = topUsers.results.map((user: any) =>
c.env.CACHE.put(`user:${user.id}`, JSON.stringify(user), {
expirationTtl: 3600,
})
);
await Promise.all(promises);
return c.json({
success: true,
message: `Warmed cache with ${topUsers.results.length} users`,
count: topUsers.results.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cache Statistics
// ============================================================================
/**
* Track cache hit/miss rates
*/
let cacheStats = {
hits: 0,
misses: 0,
errors: 0,
};
app.get('/api/cache/stats', (c) => {
const total = cacheStats.hits + cacheStats.misses;
const hitRate = total > 0 ? (cacheStats.hits / total) * 100 : 0;
return c.json({
success: true,
stats: {
hits: cacheStats.hits,
misses: cacheStats.misses,
errors: cacheStats.errors,
total,
hitRate: `${hitRate.toFixed(2)}%`,
},
});
});
// Reset stats
app.post('/api/cache/stats/reset', (c) => {
cacheStats = { hits: 0, misses: 0, errors: 0 };
return c.json({
success: true,
message: 'Cache stats reset',
});
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;
/**
* Cloudflare Workers KV - List & Pagination Patterns
*
* This template demonstrates:
* - Basic listing with cursor pagination
* - Prefix filtering
* - Async iterator pattern
* - Batch processing
* - Key search and filtering
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Basic Pagination
// ============================================================================
// List keys with cursor pagination
app.get('/kv/list', async (c) => {
const prefix = c.req.query('prefix') || '';
const cursor = c.req.query('cursor');
const limit = parseInt(c.req.query('limit') || '100', 10);
try {
const result = await c.env.MY_KV.list({
prefix,
limit: Math.min(limit, 1000), // Max 1000
cursor: cursor || undefined,
});
return c.json({
success: true,
keys: result.keys.map((k) => ({
name: k.name,
expiration: k.expiration,
metadata: k.metadata,
})),
count: result.keys.length,
hasMore: !result.list_complete,
nextCursor: result.cursor,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Async Iterator Pattern
// ============================================================================
/**
* Async generator for paginating through all keys
*/
async function* paginateKeys(
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);
}
// Get all keys (fully paginated)
app.get('/kv/all', async (c) => {
const prefix = c.req.query('prefix') || '';
try {
const allKeys: any[] = [];
// Use async iterator to get all keys
for await (const batch of paginateKeys(c.env.MY_KV, { prefix })) {
allKeys.push(...batch);
}
return c.json({
success: true,
keys: allKeys.map((k) => k.name),
totalCount: allKeys.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Prefix Filtering
// ============================================================================
// List keys by namespace prefix
app.get('/kv/namespace/:namespace', async (c) => {
const namespace = c.req.param('namespace');
const cursor = c.req.query('cursor');
try {
const result = await c.env.MY_KV.list({
prefix: `${namespace}:`, // e.g., "user:", "session:", "cache:"
cursor: cursor || undefined,
});
return c.json({
success: true,
namespace,
keys: result.keys,
count: result.keys.length,
hasMore: !result.list_complete,
nextCursor: result.cursor,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Count keys by prefix
app.get('/kv/count/:prefix', async (c) => {
const prefix = c.req.param('prefix');
try {
let count = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({
prefix,
cursor,
});
count += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
prefix,
count,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Batch Processing
// ============================================================================
/**
* Process keys in batches
*/
async function processBatches<T>(
kv: KVNamespace,
options: {
prefix?: string;
batchSize?: number;
},
processor: (keys: any[]) => Promise<T[]>
): Promise<T[]> {
const results: T[] = [];
let cursor: string | undefined;
do {
const result = await kv.list({
prefix: options.prefix,
limit: options.batchSize || 100,
cursor,
});
// Process this batch
const batchResults = await processor(result.keys);
results.push(...batchResults);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return results;
}
// Example: Export all keys with values
app.get('/kv/export', async (c) => {
const prefix = c.req.query('prefix') || '';
try {
const exported = await processBatches(
c.env.MY_KV,
{ prefix, batchSize: 100 },
async (keys) => {
// Get values for all keys in batch (bulk read)
const keyNames = keys.map((k) => k.name);
const values = await c.env.MY_KV.get(keyNames);
// Combine keys with values
return keys.map((key) => ({
key: key.name,
value: values.get(key.name),
metadata: key.metadata,
expiration: key.expiration,
}));
}
);
return c.json({
success: true,
data: exported,
count: exported.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Search & Filtering
// ============================================================================
// Search keys by pattern (client-side filtering)
app.get('/kv/search', async (c) => {
const query = c.req.query('q') || '';
const prefix = c.req.query('prefix') || '';
if (!query) {
return c.json(
{
success: false,
error: 'Query parameter "q" is required',
},
400
);
}
try {
const matches: any[] = [];
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({
prefix,
cursor,
});
// Filter keys that match the search query
const filteredKeys = result.keys.filter((key) =>
key.name.toLowerCase().includes(query.toLowerCase())
);
matches.push(...filteredKeys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
query,
matches: matches.map((k) => k.name),
count: matches.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Filter by metadata
app.get('/kv/filter/metadata', async (c) => {
const metadataKey = c.req.query('key');
const metadataValue = c.req.query('value');
if (!metadataKey) {
return c.json(
{
success: false,
error: 'Query parameter "key" is required',
},
400
);
}
try {
const matches: any[] = [];
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
// Filter by metadata
const filteredKeys = result.keys.filter((key) => {
if (!key.metadata) return false;
return metadataValue
? key.metadata[metadataKey] === metadataValue
: metadataKey in key.metadata;
});
matches.push(...filteredKeys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
matches: matches.map((k) => ({
name: k.name,
metadata: k.metadata,
})),
count: matches.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Cleanup & Maintenance
// ============================================================================
// Delete expired keys (manual cleanup)
app.post('/kv/cleanup/expired', async (c) => {
try {
let deletedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
// Filter keys that have expired
const now = Math.floor(Date.now() / 1000);
const expiredKeys = result.keys
.filter((key) => key.expiration && key.expiration < now)
.map((key) => key.name);
// Delete expired keys
await Promise.all(expiredKeys.map((key) => c.env.MY_KV.delete(key)));
deletedCount += expiredKeys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Deleted ${deletedCount} expired keys`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Delete all keys with prefix (DANGEROUS!)
app.post('/kv/delete/prefix', async (c) => {
const { prefix } = await c.req.json<{ prefix: string }>();
if (!prefix) {
return c.json(
{
success: false,
error: 'Prefix is required',
},
400
);
}
try {
let deletedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ prefix, cursor });
// Delete batch
await Promise.all(result.keys.map((key) => c.env.MY_KV.delete(key.name)));
deletedCount += result.keys.length;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Deleted ${deletedCount} keys with prefix "${prefix}"`,
deletedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Namespace Statistics
// ============================================================================
// Get detailed namespace stats
app.get('/kv/stats/detailed', async (c) => {
try {
let totalKeys = 0;
let withMetadata = 0;
let withExpiration = 0;
const prefixes = new Map<string, number>();
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
totalKeys += result.keys.length;
// Analyze keys
for (const key of result.keys) {
if (key.metadata) withMetadata++;
if (key.expiration) withExpiration++;
// Extract prefix (before first ":")
const prefix = key.name.split(':')[0];
prefixes.set(prefix, (prefixes.get(prefix) || 0) + 1);
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
stats: {
totalKeys,
withMetadata,
withExpiration,
prefixCounts: Object.fromEntries(prefixes),
},
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Group keys by prefix
app.get('/kv/groups', async (c) => {
try {
const groups = new Map<string, string[]>();
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ cursor });
for (const key of result.keys) {
const prefix = key.name.split(':')[0];
if (!groups.has(prefix)) {
groups.set(prefix, []);
}
groups.get(prefix)!.push(key.name);
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
groups: Object.fromEntries(groups),
groupCount: groups.size,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;
/**
* Cloudflare Workers KV - Metadata Patterns
*
* This template demonstrates:
* - Storing data in metadata for list() efficiency
* - Metadata-based filtering
* - Versioning with metadata
* - Audit trails
* - Feature flags with metadata
*/
import { Hono } from 'hono';
type Bindings = {
MY_KV: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Metadata Optimization Pattern
// ============================================================================
/**
* Store small values in metadata to avoid separate get() calls
* Maximum metadata size: 1024 bytes (JSON serialized)
*/
// ❌ BAD: Requires 2 operations per key
async function getStatusBad(kv: KVNamespace, userId: string) {
const status = await kv.get(`user:${userId}:status`);
const lastSeen = await kv.get(`user:${userId}:lastseen`);
return { status, lastSeen };
}
// ✅ GOOD: Single list() operation gets metadata for all users
async function getStatusGood(kv: KVNamespace) {
const users = await kv.list({ prefix: 'user:' });
return users.keys.map((key) => ({
userId: key.name.split(':')[1],
status: key.metadata?.status,
lastSeen: key.metadata?.lastSeen,
}));
}
// Example: User status with metadata
app.post('/users/:id/status', async (c) => {
const userId = c.req.param('id');
const { status } = await c.req.json<{ status: string }>();
try {
// Store empty value, all data in metadata
await c.env.MY_KV.put(`user:${userId}`, '', {
metadata: {
status,
lastSeen: Date.now(),
plan: 'free',
},
});
return c.json({
success: true,
message: 'Status updated',
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all users with status (no additional get() calls needed!)
app.get('/users/status', async (c) => {
try {
const users = await c.env.MY_KV.list({ prefix: 'user:' });
const statuses = users.keys.map((key) => ({
userId: key.name.split(':')[1],
status: key.metadata?.status || 'unknown',
lastSeen: key.metadata?.lastSeen,
plan: key.metadata?.plan,
}));
return c.json({
success: true,
users: statuses,
count: statuses.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Versioning with Metadata
// ============================================================================
interface VersionedData {
content: any;
version: number;
updatedAt: number;
updatedBy: string;
}
// Write with versioning
app.put('/config/:key', async (c) => {
const key = c.req.param('key');
const content = await c.req.json();
const updatedBy = c.req.header('X-User-ID') || 'system';
try {
// Get current version
const existing = await c.env.MY_KV.getWithMetadata<
VersionedData,
{ version: number }
>(`config:${key}`, { type: 'json' });
const currentVersion = existing.metadata?.version || 0;
const newVersion = currentVersion + 1;
// Store new version
const data: VersionedData = {
content,
version: newVersion,
updatedAt: Date.now(),
updatedBy,
};
await c.env.MY_KV.put(`config:${key}`, JSON.stringify(data), {
metadata: {
version: newVersion,
updatedAt: data.updatedAt,
updatedBy,
},
});
// Store version history (optional)
await c.env.MY_KV.put(
`config:${key}:v${newVersion}`,
JSON.stringify(data),
{
expirationTtl: 86400 * 30, // Keep versions for 30 days
}
);
return c.json({
success: true,
version: newVersion,
previousVersion: currentVersion,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get config with version info
app.get('/config/:key', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
VersionedData,
{ version: number; updatedAt: number; updatedBy: string }
>(`config:${key}`, { type: 'json' });
if (!value) {
return c.json(
{
success: false,
error: 'Config not found',
},
404
);
}
return c.json({
success: true,
data: value,
metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Get specific version
app.get('/config/:key/version/:version', async (c) => {
const key = c.req.param('key');
const version = c.req.param('version');
try {
const data = await c.env.MY_KV.get<VersionedData>(
`config:${key}:v${version}`,
{ type: 'json' }
);
if (!data) {
return c.json(
{
success: false,
error: `Version ${version} not found`,
},
404
);
}
return c.json({
success: true,
data,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Audit Trail with Metadata
// ============================================================================
interface AuditMetadata {
createdBy: string;
createdAt: number;
updatedBy: string;
updatedAt: number;
accessCount: number;
}
// Write with audit trail
app.post('/data/:key', async (c) => {
const key = c.req.param('key');
const value = await c.req.text();
const userId = c.req.header('X-User-ID') || 'anonymous';
try {
// Check if key exists
const existing = await c.env.MY_KV.getWithMetadata<string, AuditMetadata>(
key
);
const metadata: AuditMetadata = existing.metadata
? {
...existing.metadata,
updatedBy: userId,
updatedAt: Date.now(),
}
: {
createdBy: userId,
createdAt: Date.now(),
updatedBy: userId,
updatedAt: Date.now(),
accessCount: 0,
};
await c.env.MY_KV.put(key, value, { metadata });
return c.json({
success: true,
audit: metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Read with access tracking
app.get('/data/:key', async (c) => {
const key = c.req.param('key');
try {
const { value, metadata } = await c.env.MY_KV.getWithMetadata<
string,
AuditMetadata
>(key);
if (!value) {
return c.json(
{
success: false,
error: 'Key not found',
},
404
);
}
// Increment access count (fire-and-forget)
if (metadata) {
c.executionCtx.waitUntil(
c.env.MY_KV.put(key, value, {
metadata: {
...metadata,
accessCount: metadata.accessCount + 1,
},
})
);
}
return c.json({
success: true,
value,
audit: metadata,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Feature Flags with Metadata
// ============================================================================
interface FeatureFlag {
enabled: boolean;
rolloutPercentage: number;
targetUsers?: string[];
metadata: {
createdAt: number;
updatedAt: number;
description: string;
};
}
// Create feature flag
app.post('/flags/:name', async (c) => {
const name = c.req.param('name');
const flag = await c.req.json<FeatureFlag>();
try {
await c.env.MY_KV.put(`flag:${name}`, JSON.stringify(flag), {
metadata: {
enabled: flag.enabled,
rolloutPercentage: flag.rolloutPercentage,
updatedAt: Date.now(),
},
});
return c.json({
success: true,
message: `Feature flag "${name}" created`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// List all feature flags (metadata only)
app.get('/flags', async (c) => {
try {
const flags = await c.env.MY_KV.list({ prefix: 'flag:' });
const flagList = flags.keys.map((key) => ({
name: key.name.replace('flag:', ''),
enabled: key.metadata?.enabled || false,
rolloutPercentage: key.metadata?.rolloutPercentage || 0,
updatedAt: key.metadata?.updatedAt,
}));
return c.json({
success: true,
flags: flagList,
count: flagList.length,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Check feature flag for user
app.get('/flags/:name/check/:userId', async (c) => {
const name = c.req.param('name');
const userId = c.req.param('userId');
try {
const flag = await c.env.MY_KV.get<FeatureFlag>(`flag:${name}`, {
type: 'json',
});
if (!flag) {
return c.json(
{
success: false,
error: 'Feature flag not found',
},
404
);
}
// Check if enabled
if (!flag.enabled) {
return c.json({ enabled: false, reason: 'Flag disabled globally' });
}
// Check target users
if (flag.targetUsers && flag.targetUsers.length > 0) {
const enabled = flag.targetUsers.includes(userId);
return c.json({
enabled,
reason: enabled ? 'User in target list' : 'User not in target list',
});
}
// Check rollout percentage
const userHash =
parseInt(userId.split('').reduce((a, b) => a + b.charCodeAt(0), 0).toString()) %
100;
const enabled = userHash < flag.rolloutPercentage;
return c.json({
enabled,
reason: enabled
? `User in ${flag.rolloutPercentage}% rollout`
: `User not in ${flag.rolloutPercentage}% rollout`,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// ============================================================================
// Metadata Size Validation
// ============================================================================
// Validate metadata size before writing
app.post('/validate/metadata', async (c) => {
const { metadata } = await c.req.json<{ metadata: any }>();
const serialized = JSON.stringify(metadata);
const size = new TextEncoder().encode(serialized).length;
if (size > 1024) {
return c.json({
valid: false,
size,
maxSize: 1024,
error: `Metadata too large: ${size} bytes (max 1024)`,
});
}
return c.json({
valid: true,
size,
maxSize: 1024,
});
});
// ============================================================================
// Metadata Migration
// ============================================================================
// Migrate existing keys to add metadata
app.post('/migrate/add-metadata', async (c) => {
const { prefix, metadata } = await c.req.json<{
prefix: string;
metadata: any;
}>();
try {
let migratedCount = 0;
let cursor: string | undefined;
do {
const result = await c.env.MY_KV.list({ prefix, cursor });
// Migrate batch
for (const key of result.keys) {
// Get existing value
const value = await c.env.MY_KV.get(key.name);
if (value !== null) {
// Re-write with metadata
await c.env.MY_KV.put(key.name, value, {
metadata: {
...key.metadata,
...metadata,
migratedAt: Date.now(),
},
});
migratedCount++;
}
}
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
return c.json({
success: true,
message: `Migrated ${migratedCount} keys`,
migratedCount,
});
} catch (error) {
return c.json(
{
success: false,
error: (error as Error).message,
},
500
);
}
});
// Health check
app.get('/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
});
export default app;
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"observability": {
"enabled": true
},
// KV Namespace Bindings
"kv_namespaces": [
{
// The binding name - accessible as env.CACHE in your Worker
"binding": "CACHE",
// Production namespace ID (from: wrangler kv namespace create CACHE)
"id": "<YOUR_PRODUCTION_NAMESPACE_ID>",
// Preview/local namespace ID (from: wrangler kv namespace create CACHE --preview)
// This is optional but recommended for local development
"preview_id": "<YOUR_PREVIEW_NAMESPACE_ID>"
},
// Multiple namespaces example
{
"binding": "CONFIG",
"id": "<CONFIG_PRODUCTION_ID>",
"preview_id": "<CONFIG_PREVIEW_ID>"
},
{
"binding": "SESSIONS",
"id": "<SESSIONS_PRODUCTION_ID>",
"preview_id": "<SESSIONS_PREVIEW_ID>"
}
]
// IMPORTANT NOTES:
//
// 1. Create namespaces first:
// npx wrangler kv namespace create CACHE
// npx wrangler kv namespace create CACHE --preview
//
// 2. Copy the IDs from the command output to this file
//
// 3. NEVER commit real namespace IDs to public repos
// Use environment variables for sensitive namespaces:
// "id": "${KV_CACHE_ID}"
//
// 4. preview_id is optional but recommended for local development
// It creates a separate namespace for testing
//
// 5. Binding names must be valid JavaScript identifiers
// Good: CACHE, MY_KV, UserData
// Bad: my-kv, user.data, 123kv
}