Files
2025-11-30 08:24:21 +08:00

430 lines
13 KiB
Markdown

---
name: cloudflare-kv
description: |
Store key-value data globally with Cloudflare KV's 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.
license: MIT
---
# Cloudflare Workers KV
**Status**: Production Ready ✅
**Last Updated**: 2025-11-24
**Dependencies**: cloudflare-worker-base (for Worker setup)
**Latest Versions**: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0
**Recent Updates (2025)**:
- **August 2025**: Architecture redesign (40x performance gain, <5ms p99 latency, hybrid storage with R2)
- **January 2025**: Namespace limit increased (200 → 1,000 namespaces per account for Free and Paid plans)
---
## Quick Start (5 Minutes)
```bash
# Create namespace
npx wrangler kv namespace create MY_NAMESPACE
# Output: [[kv_namespaces]] binding = "MY_NAMESPACE" id = "<UUID>"
```
**wrangler.jsonc:**
```jsonc
{
"kv_namespaces": [{
"binding": "MY_NAMESPACE", // Access as env.MY_NAMESPACE
"id": "<production-uuid>",
"preview_id": "<preview-uuid>" // Optional: local dev
}]
}
```
**Basic Usage:**
```typescript
type Bindings = { MY_NAMESPACE: KVNamespace };
app.post('/set/:key', async (c) => {
await c.env.MY_NAMESPACE.put(c.req.param('key'), await c.req.text());
return c.json({ success: true });
});
app.get('/get/:key', async (c) => {
const value = await c.env.MY_NAMESPACE.get(c.req.param('key'));
return value ? c.json({ value }) : c.json({ error: 'Not found' }, 404);
});
```
---
## KV API Reference
### Read Operations
```typescript
// Get single key
const value = await env.MY_KV.get('key'); // string | null
const data = await env.MY_KV.get('key', { type: 'json' }); // object | null
const buffer = await env.MY_KV.get('key', { type: 'arrayBuffer' });
const stream = await env.MY_KV.get('key', { type: 'stream' });
// Get with cache (minimum 60s)
const value = await env.MY_KV.get('key', { cacheTtl: 300 }); // 5 min edge cache
// Bulk read (counts as 1 operation)
const values = await env.MY_KV.get(['key1', 'key2']); // Map<string, string | null>
// With metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('key');
const result = await env.MY_KV.getWithMetadata(['key1', 'key2']); // Bulk with metadata
```
### Write Operations
```typescript
// Basic write (max 1/second per key)
await env.MY_KV.put('key', 'value');
await env.MY_KV.put('user:123', JSON.stringify({ name: 'John' }));
// With expiration
await env.MY_KV.put('session', data, { expirationTtl: 3600 }); // 1 hour
await env.MY_KV.put('token', value, { expiration: Math.floor(Date.now()/1000) + 86400 });
// With metadata (max 1024 bytes)
await env.MY_KV.put('config', 'dark', {
metadata: { updatedAt: Date.now(), version: 2 }
});
```
**Critical Limits:**
- Key: 512 bytes max
- Value: 25 MiB max
- Metadata: 1024 bytes max
- Write rate: 1/second per key (429 error if exceeded)
- Expiration: 60 seconds minimum
### List Operations
```typescript
// List with pagination
const result = await env.MY_KV.list({ prefix: 'user:', limit: 1000, cursor });
// result: { keys: [], list_complete: boolean, cursor?: string }
// CRITICAL: Always check list_complete, not keys.length === 0
let cursor: string | undefined;
do {
const result = await env.MY_KV.list({ prefix: 'user:', cursor });
processKeys(result.keys);
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
```
### Delete Operations
```typescript
// Delete single key
await env.MY_KV.delete('key'); // Always succeeds
// Bulk delete (CLI only, up to 10,000 keys)
// npx wrangler kv bulk delete --binding=MY_KV keys.json
```
---
## Advanced Patterns
### Caching Pattern with CacheTtl
```typescript
async function getCachedData(kv: KVNamespace, key: string, fetchFn: () => Promise<any>, ttl = 300) {
const cached = await kv.get(key, { type: 'json', cacheTtl: ttl });
if (cached) return cached;
const data = await fetchFn();
await kv.put(key, JSON.stringify(data), { expirationTtl: ttl * 2 });
return data;
}
```
**Guidelines**: Minimum 60s, use for read-heavy workloads (100:1 read/write ratio)
### Metadata Optimization
```typescript
// Store small values (<1024 bytes) in metadata to avoid separate get() calls
await env.MY_KV.put('user:123', '', {
metadata: { status: 'active', plan: 'pro', lastSeen: Date.now() }
});
// list() returns metadata automatically (no additional get() calls)
const users = await env.MY_KV.list({ prefix: 'user:' });
users.keys.forEach(({ name, metadata }) => console.log(name, metadata.status));
```
### Key Coalescing
```typescript
// ❌ Bad: Many cold keys
await kv.put('user:123:name', 'John');
await kv.put('user:123:email', 'john@example.com');
// ✅ Good: Single hot key
await kv.put('user:123', JSON.stringify({ name: 'John', email: 'john@example.com' }));
```
**Benefit**: Cold keys benefit from hot key caching, fewer operations
**Trade-off**: Requires read-modify-write for updates
### Pagination Helper
```typescript
async function* paginateKV(kv: KVNamespace, options: { prefix?: string } = {}) {
let cursor: string | undefined;
do {
const result = await kv.list({ ...options, cursor });
yield result.keys;
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
}
// Usage
for await (const keys of paginateKV(env.MY_KV, { prefix: 'user:' })) {
processKeys(keys);
}
```
### Rate Limit Retry with Exponential Backoff
```typescript
async function putWithRetry(kv: KVNamespace, key: string, value: string, opts?: KVPutOptions) {
let attempts = 0, delay = 1000;
while (attempts < 5) {
try {
await kv.put(key, value, opts);
return;
} catch (error) {
if ((error as Error).message.includes('429')) {
attempts++;
if (attempts >= 5) throw new Error('Max retry attempts');
await new Promise(r => setTimeout(r, delay));
delay *= 2; // Exponential backoff
} else throw error;
}
}
}
```
---
## Understanding Eventual Consistency
KV is **eventually consistent** across Cloudflare's global network (Aug 2025 redesign: hybrid storage, <5ms p99 latency):
**How It Works:**
1. Writes immediately visible in same location
2. Other locations see update within ~60 seconds (or cacheTtl value)
3. Cached reads may return stale data during propagation
**Example:**
```typescript
// Tokyo: Write
await env.MY_KV.put('counter', '1');
const value = await env.MY_KV.get('counter'); // "1" ✅
// London (within 60s): May be stale ⚠️
const value2 = await env.MY_KV.get('counter'); // Might be old value
// After 60+ seconds: Consistent ✅
```
**Use KV for**: Read-heavy workloads (100:1 ratio), config, feature flags, caching, user preferences
**Don't use KV for**: Financial transactions, strong consistency, >1/second writes per key, critical data
**Need strong consistency?** Use [Durable Objects](https://developers.cloudflare.com/durable-objects/)
---
## Wrangler CLI Essentials
```bash
# Create namespace
npx wrangler kv namespace create MY_NAMESPACE [--preview]
# Manage keys
npx wrangler kv key put --binding=MY_KV "key" "value" [--ttl=3600] [--metadata='{}']
npx wrangler kv key get --binding=MY_KV "key"
npx wrangler kv key list --binding=MY_KV [--prefix="user:"]
npx wrangler kv key delete --binding=MY_KV "key"
# Bulk operations (up to 10,000 keys)
npx wrangler kv bulk put --binding=MY_KV data.json
npx wrangler kv bulk delete --binding=MY_KV keys.json
```
---
## 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 |
| Key size | 512 bytes | 512 bytes |
| Metadata size | 1024 bytes | 1024 bytes |
| Value size | 25 MiB | 25 MiB |
| Minimum cacheTtl | 60 seconds | 60 seconds |
**Critical**: 1 write/second per key (429 if exceeded), bulk operations count as 1 operation, namespace limit increased from 200 → 1,000 (Jan 2025)
---
## Error Handling
### 1. Rate Limit (429 Too Many Requests)
**Cause**: Writing to same key >1/second
**Solution**: Use retry with exponential backoff (see Advanced Patterns)
```typescript
// ❌ Bad
await env.MY_KV.put('counter', '1');
await env.MY_KV.put('counter', '2'); // 429 error!
// ✅ Good
await putWithRetry(env.MY_KV, 'counter', '2');
```
### 2. Value Too Large
**Cause**: Value exceeds 25 MiB
**Solution**: Validate size before writing
```typescript
if (value.length > 25 * 1024 * 1024) throw new Error('Value exceeds 25 MiB');
```
### 3. Metadata Too Large
**Cause**: Metadata exceeds 1024 bytes when serialized
**Solution**: Validate serialized size
```typescript
const serialized = JSON.stringify(metadata);
if (serialized.length > 1024) throw new Error('Metadata exceeds 1024 bytes');
```
### 4. Invalid CacheTtl
**Cause**: cacheTtl <60 seconds
**Solution**: Use minimum 60
```typescript
// ❌ Error
await env.MY_KV.get('key', { cacheTtl: 30 });
// ✅ Correct
await env.MY_KV.get('key', { cacheTtl: 60 });
```
---
## Critical Rules
### Always Do ✅
1. Use bulk operations when reading multiple keys (counts as 1 operation)
2. Set cacheTtl for frequently-read, infrequently-updated data (min 60s)
3. Store small values (<1024 bytes) 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 512B, value 25MiB, metadata 1KB)
7. Coalesce related keys for better caching performance
8. 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 = no guarantees)
8. Never forget to handle null values (`get()` returns `null` if key doesn't exist)
---
## Troubleshooting
### Issue 1: "429 Too Many Requests" on writes
**Cause**: Writing to same key >1/second
**Solution**: Consolidate writes or use retry with exponential backoff
```typescript
// ❌ Bad: Rate limit
for (let i = 0; i < 10; i++) await kv.put('counter', String(i));
// ✅ Good: Single write
await kv.put('counter', '9');
// ✅ Good: Retry with backoff
await putWithRetry(kv, 'counter', String(i));
```
### Issue 2: Stale reads after write
**Cause**: Eventual consistency (~60 seconds propagation)
**Solution**: Accept stale reads, use Durable Objects for strong consistency, or implement app-level cache invalidation
### Issue 3: "Operations limit exceeded"
**Cause**: >1000 KV operations in single Worker invocation
**Solution**: Use bulk operations
```typescript
// ❌ Bad: 5000 operations
for (const key of 5000keys) await kv.get(key);
// ✅ Good: 1 operation
const values = await kv.get(keys); // Bulk read
```
### Issue 4: List returns empty but cursor exists
**Cause**: Deleted/expired keys create "tombstones"
**Solution**: Always check `list_complete`, not `keys.length`
```typescript
// ✅ Correct pagination
let cursor: string | undefined;
do {
const result = await kv.list({ cursor });
processKeys(result.keys); // Even if empty
cursor = result.list_complete ? undefined : result.cursor;
} while (cursor);
```
---
## Production Checklist
- [ ] Environment-specific namespaces configured (`id` vs `preview_id`)
- [ ] Namespace IDs stored in environment variables (not hardcoded)
- [ ] Rate limit retry logic implemented for writes
- [ ] Appropriate `cacheTtl` values set for reads (min 60s)
- [ ] Sizes validated (key 512B, value 25MiB, metadata 1KB)
- [ ] Bulk operations used where possible
- [ ] Pagination with `list_complete` check (not `keys.length`)
- [ ] Error handling for null values
- [ ] Monitoring/alerting for rate limits
---
## Related Documentation
- [Cloudflare KV Docs](https://developers.cloudflare.com/kv/)
- [KV API Reference](https://developers.cloudflare.com/kv/api/)
- [KV Limits](https://developers.cloudflare.com/kv/platform/limits/)
- [How KV Works](https://developers.cloudflare.com/kv/concepts/how-kv-works/)
- [Wrangler KV Commands](https://developers.cloudflare.com/workers/wrangler/commands/#kv)
---
**Last Updated**: 2025-11-24
**Package Versions**: wrangler@4.50.0, @cloudflare/workers-types@4.20251121.0