Files
gh-jezweb-claude-skills-ski…/references/best-practices.md
2025-11-30 08:24:21 +08:00

721 lines
14 KiB
Markdown

# 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>('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<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
```typescript
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
```typescript
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: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<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
```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<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
```typescript
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
```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<SessionData>(`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<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
```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<AppConfig>('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/)