# Cloudflare Workers KV - Best Practices This document contains production-tested best practices for Cloudflare Workers KV. --- ## Table of Contents 1. [Performance Optimization](#performance-optimization) 2. [Caching Strategies](#caching-strategies) 3. [Key Design](#key-design) 4. [Metadata Usage](#metadata-usage) 5. [Error Handling](#error-handling) 6. [Security](#security) 7. [Cost Optimization](#cost-optimization) 8. [Monitoring & Debugging](#monitoring--debugging) --- ## Performance Optimization ### 1. Use Bulk Operations **❌ Bad: Individual reads** ```typescript 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** ```typescript const values = await kv.get(['key1', 'key2', 'key3']); // 1 operation // Total: 1 operation ``` **Benefits:** - Counts as 1 operation against the 1000/invocation limit - Faster execution - Lower latency --- ### 2. Use CacheTtl for Frequently-Read Data **❌ Bad: No edge caching** ```typescript const value = await kv.get('config'); // Fetches from KV every time ``` **✅ Good: Edge caching** ```typescript const value = await kv.get('config', { cacheTtl: 300, // Cache at edge for 5 minutes }); ``` **Guidelines:** - Use `cacheTtl` for 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** ```typescript 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** ```typescript 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: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** ```typescript 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** ```typescript 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) ```typescript async function getCached( kv: KVNamespace, key: string, fetchFn: () => Promise, ttl = 3600 ): Promise { // Try cache const cached = await kv.get(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 ```typescript async function updateCached( kv: KVNamespace, key: string, data: T, ttl = 3600 ): Promise { // 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 ```typescript async function staleWhileRevalidate( kv: KVNamespace, key: string, fetchFn: () => Promise, ctx: ExecutionContext, staleThreshold = 300 ): Promise { const { value, metadata } = await kv.getWithMetadata( 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:hash123 ``` **Benefits:** - 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: ```typescript // 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 ```typescript await kv.put('config', JSON.stringify(data), { metadata: { version: 2, updatedAt: Date.now(), updatedBy: 'admin', }, }); ``` --- ### 2. Audit Trails ```typescript await kv.put(key, value, { metadata: { createdAt: Date.now(), createdBy: userId, accessCount: 0, }, }); ``` --- ### 3. Feature Flags ```typescript // 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) ```typescript async function putWithRetry( kv: KVNamespace, key: string, value: string, maxAttempts = 5 ): Promise { 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 ```typescript // ❌ 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 ```typescript 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:** ```jsonc { "kv_namespaces": [ { "binding": "MY_KV", "id": "abc123def456..." // Hardcoded! } ] } ``` **✅ Good:** ```jsonc { "kv_namespaces": [ { "binding": "MY_KV", "id": "${KV_NAMESPACE_ID}" // Environment variable } ] } ``` --- ### 2. Encrypt Sensitive Data ```typescript // 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 ```jsonc { "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** ```typescript 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** ```typescript await kv.put('user:123', '', { metadata: { status: 'active', plan: 'pro', updated: Date.now() }, }); ``` --- ### 3. Set Appropriate TTLs Longer TTLs = fewer rewrites = lower costs ```typescript // ❌ 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 ```typescript let stats = { hits: 0, misses: 0 }; async function getCached(kv: KVNamespace, key: string): Promise { const value = await kv.get(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 ```typescript async function loggedGet( kv: KVNamespace, key: string ): Promise { const start = Date.now(); const value = await kv.get(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 ```typescript 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 `cacheTtl` values 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 ```typescript // 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(`session:${sessionId}`, { type: 'json', cacheTtl: 60, // Cache for 1 minute }); ``` --- ### 2. API Response Caching ```typescript const cacheKey = `api:${endpoint}:${JSON.stringify(params)}`; let response = await kv.get(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 ```typescript // 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('config:app', { type: 'json', cacheTtl: 3600, // Cache for 1 hour }); ``` --- ## References - [Cloudflare KV Documentation](https://developers.cloudflare.com/kv/) - [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/) - [KV Limits](https://developers.cloudflare.com/kv/platform/limits/) - [KV Pricing](https://developers.cloudflare.com/kv/platform/pricing/)