
Cloudflare Kv
- 144 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
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
- 144 all-time installs (skills.sh)
- +12 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,422 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-kvAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 144 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Cloudflare Workers KV
Status: Production Ready ✅ | Last Verified: 2025-12-27
---
What Is Workers KV?
Global key-value storage on Cloudflare edge:
- Eventually consistent
- Low latency worldwide
- 1GB+ values supported
- TTL expiration
- Metadata support
---
Quick Start (5 Minutes)
1. Create KV Namespace
bunx wrangler kv namespace create MY_NAMESPACE
bunx wrangler kv namespace create MY_NAMESPACE --preview2. Configure Binding
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"kv_namespaces": [
{
"binding": "MY_NAMESPACE",
"id": "<PRODUCTION_ID>",
"preview_id": "<PREVIEW_ID>"
}
]
}3. Basic Operations
export default {
async fetch(request, env, ctx) {
// Write
await env.MY_NAMESPACE.put('key', 'value');
// Read
const value = await env.MY_NAMESPACE.get('key');
// Delete
await env.MY_NAMESPACE.delete('key');
return new Response(value);
}
};Load `references/setup-guide.md` for complete setup.
---
KV API Methods
put() - Write
// Basic
await env.MY_NAMESPACE.put('key', 'value');
// With TTL (1 hour)
await env.MY_NAMESPACE.put('key', 'value', {
expirationTtl: 3600
});
// With expiration timestamp
await env.MY_NAMESPACE.put('key', 'value', {
expiration: Math.floor(Date.now() / 1000) + 3600
});
// With metadata
await env.MY_NAMESPACE.put('key', 'value', {
metadata: { role: 'admin', created: Date.now() }
});get() - Read
// Simple get
const value = await env.MY_NAMESPACE.get('key');
// With type
const text = await env.MY_NAMESPACE.get('key', 'text');
const json = await env.MY_NAMESPACE.get('key', 'json');
const buffer = await env.MY_NAMESPACE.get('key', 'arrayBuffer');
const stream = await env.MY_NAMESPACE.get('key', 'stream');
// With metadata
const { value, metadata } = await env.MY_NAMESPACE.getWithMetadata('key');delete() - Remove
await env.MY_NAMESPACE.delete('key');list() - List Keys
// Basic list
const { keys } = await env.MY_NAMESPACE.list();
// With prefix
const { keys } = await env.MY_NAMESPACE.list({
prefix: 'user:',
limit: 100
});
// Pagination
const { keys, cursor } = await env.MY_NAMESPACE.list({
cursor: previousCursor
});---
Critical Rules
Always Do ✅
1. Use TTL for temporary data 2. Handle null (key might not exist) 3. Use metadata for small data 4. Paginate lists (max 1000 keys) 5. Use prefixes for organization 6. Cache in Worker (avoid multiple KV calls) 7. Use waitUntil() for async writes 8. Handle eventual consistency 9. Monitor rate limits 10. Use JSON.stringify for objects
Never Do ❌
1. Never assume instant consistency 2. Never exceed 25MB per value 3. Never list all keys without pagination 4. Never skip error handling 5. Never use for real-time data 6. Never exceed rate limits (1000 writes/second) 7. Never store secrets unencrypted 8. Never use as database (no transactions) 9. Never ignore metadata limits (1024 bytes) 10. Never skip TTL for temporary data
---
Common Use Cases
Use Case 1: API Response Caching
const cacheKey = `api:${url}`;
let cached = await env.MY_NAMESPACE.get(cacheKey, 'json');
if (!cached) {
cached = await fetch(url).then(r => r.json());
await env.MY_NAMESPACE.put(cacheKey, JSON.stringify(cached), {
expirationTtl: 300 // 5 minutes
});
}
return Response.json(cached);Use Case 2: User Preferences
const userId = '123';
const preferences = {
theme: 'dark',
language: 'en'
};
await env.MY_NAMESPACE.put(
`user:${userId}:preferences`,
JSON.stringify(preferences),
{
metadata: { updated: Date.now() }
}
);Use Case 3: Rate Limiting
const key = `ratelimit:${ip}`;
const count = parseInt(await env.MY_NAMESPACE.get(key) || '0');
if (count >= 100) {
return new Response('Rate limit exceeded', { status: 429 });
}
await env.MY_NAMESPACE.put(key, String(count + 1), {
expirationTtl: 60 // 1 minute window
});Use Case 4: List with Prefix
const { keys } = await env.MY_NAMESPACE.list({
prefix: 'user:',
limit: 100
});
const users = await Promise.all(
keys.map(({ name }) => env.MY_NAMESPACE.get(name, 'json'))
);Use Case 5: waitUntil() Pattern
export default {
async fetch(request, env, ctx) {
// Don't wait for KV write
ctx.waitUntil(
env.MY_NAMESPACE.put('analytics', JSON.stringify(data))
);
return new Response('OK');
}
};---
Limits (Summary)
Key Limits:
- Key size: 512 bytes max
- Value size: 25 MB max
- Metadata: 1024 bytes max
Rate Limits:
- Writes: 1000/sec per key
- List: 100/sec per namespace
- Reads: Unlimited
For detailed limits, pricing, and optimization strategies, load `references/limits-quotas.md`
---
Eventual Consistency
KV is eventually consistent:
- Writes propagate globally (~60 seconds)
- Not suitable for real-time data
- Use D1 for strong consistency
Pattern:
// Write
await env.MY_NAMESPACE.put('key', 'value');
// May not be visible immediately in other regions
const value = await env.MY_NAMESPACE.get('key'); // Might be null---
When to Load References
Load specific reference files based on task context:
For Setup & Configuration:
- Load
references/setup-guide.mdwhen creating namespaces or configuring bindings
For Performance Optimization:
- Load
references/best-practices.mdwhen implementing caching or optimizing performance - Load
references/performance-tuning.mdfor advanced optimization scenarios, cacheTtl strategies, or benchmarking
For API Usage:
- Load
references/workers-api.mdwhen implementing KV operations or need method signatures
For Troubleshooting:
- Load
references/troubleshooting.mdwhen debugging errors or consistency issues
For Limits & Quotas:
- Load
references/limits-quotas.mdwhen planning capacity or encountering quota errors
For Migration:
- Load
references/migration-guide.mdwhen migrating from localStorage, Redis, D1, R2, or other storage solutions
---
Resources
References (references/):
best-practices.md- Production patterns, caching strategies, rate limit handling, error recoverysetup-guide.md- Complete setup with Wrangler CLI commands, namespace creation, bindings configurationworkers-api.md- Complete API reference, consistency model (eventual consistency), limits & quotas, performance optimizationtroubleshooting.md- Comprehensive error catalog with solutionslimits-quotas.md- Detailed limits, quotas, pricing, and optimization tipsmigration-guide.md- Complete migration guides from localStorage, Redis, D1, R2, and other storage solutionsperformance-tuning.md- Advanced cacheTtl strategies, bulk operations, key design, benchmarking techniques
Templates (templates/):
kv-basic-operations.ts- Basic KV operations (get, put, delete, list)kv-caching-pattern.ts- HTTP caching with KVkv-list-pagination.ts- List with cursor paginationkv-metadata-pattern.ts- Metadata usage patternswrangler-kv-config.jsonc- KV namespace bindings
Scripts (scripts/):
check-versions.sh- Validate KV API endpoints and package versionstest-kv-connection.sh- Test KV namespace connection and operationssetup-kv-namespace.sh- Interactive namespace setup wizardvalidate-kv-config.sh- Validate wrangler.jsonc configurationanalyze-kv-usage.sh- Analyze code for KV usage patterns and optimizations
Commands:
/cloudflare-kv:setup- Interactive KV namespace setup wizard/cloudflare-kv:test- Test KV operations and connection/cloudflare-kv:optimize- Analyze and optimize KV usage
Agents:
kv-optimizer- Analyzes KV usage and suggests performance optimizationskv-debugger- Helps debug KV errors and consistency issues
Examples (examples/):
rate-limiting/- Complete rate limiting implementation (fixed window, sliding window, token bucket, multi-tier)session-management/- Production session store with TTL expiration, metadata tracking, and admin controlsapi-caching/- HTTP response caching patterns (cache-aside, stale-while-revalidate, conditional caching, ETag)config-management/- Feature flags, A/B testing, environment configs, version tracking, hot-reload
---
Official Documentation
- KV Overview: https://developers.cloudflare.com/kv/
- KV API: https://developers.cloudflare.com/kv/api/
- Best Practices: https://developers.cloudflare.com/kv/best-practices/
---
Questions? Issues?
1. Check references/setup-guide.md for complete setup 2. Verify namespace binding configured 3. Handle eventual consistency 4. Check rate limits
KV Debugger Agent
Autonomous agent specialized in debugging Cloudflare Workers KV errors, diagnosing configuration issues, and providing step-by-step solutions for common problems.
Agent Capabilities
Error Diagnosis
- Identifies KV_ERROR types and root causes
- Analyzes 429 rate limit issues
- Debugs eventual consistency problems
- Validates namespace bindings
- Checks configuration correctness
- Investigates timeout errors
- Diagnoses permission issues
Configuration Validation
- Verifies wrangler.jsonc syntax
- Validates namespace IDs
- Checks binding names
- Confirms environment setup
- Tests authentication status
Solution Provision
- Provides error-specific fixes
- Offers step-by-step recovery procedures
- Suggests preventive measures
- Recommends monitoring strategies
Automated Testing
- Runs connection tests
- Validates CRUD operations
- Checks rate limit compliance
- Verifies configuration integrity
When to Use This Agent
The agent triggers when users mention:
- "kv error"
- "KV_ERROR"
- "429 too many requests"
- "kv rate limit"
- "kv not working"
- "namespace not found"
- "eventual consistency"
- "kv timeout"
- "binding error"
- "kv undefined"
Agent Workflow
Phase 1: Error Identification
1. Gather Error Context
- Ask user for error message
- Request relevant code snippet
- Get wrangler.jsonc configuration
- Determine when error occurs (dev/production)
2. Categorize Error
- Configuration error (wrong binding, missing namespace)
- Runtime error (KV_ERROR, timeout, rate limit)
- Logic error (eventual consistency, null values)
- Permission error (authentication, API access)
Phase 2: Diagnosis
3. Validate Configuration
- Run validate-kv-config.sh:
${CLAUDE_PLUGIN_ROOT}/scripts/validate-kv-config.sh- Check wrangler.jsonc for issues
- Verify namespace ID format
- Confirm binding names
4. Test Connection
- Run test-kv-connection.sh:
${CLAUDE_PLUGIN_ROOT}/scripts/test-kv-connection.sh <namespace>- Verify basic CRUD operations
- Identify failing operation
5. Load Troubleshooting Knowledge
- Load
references/troubleshooting.mdfor error catalog - Match error to known issues
- Identify solution pattern
Phase 3: Solution
6. Provide Fix
- Explain root cause
- Offer step-by-step solution
- Provide corrected code examples
- Suggest preventive measures
7. Validate Fix
- Test proposed solution if possible
- Verify configuration changes
- Confirm error resolution
8. Monitor
- Recommend monitoring strategies
- Suggest logging improvements
- Provide debugging tips for future
Tools Available to Agent
- Read - Read configuration and code files
- Grep - Search for error patterns
- Bash - Execute test and validation scripts
- Edit - Fix configuration issues (with approval)
Common Error Scenarios
Error 1: "KV namespace not found"
Diagnosis Flow: 1. Check if binding exists in wrangler.jsonc 2. Verify namespace ID is correct 3. Confirm wrangler authentication 4. Test namespace accessibility
Solution Pattern:
Issue: The binding 'MY_KV' is not defined in wrangler.jsonc
Fix:
1. Add to wrangler.jsonc:
"kv_namespaces": [{
"binding": "MY_KV",
"id": "your-namespace-id"
}]
2. Get namespace ID:
wrangler kv namespace list
3. Test configuration:
${CLAUDE_PLUGIN_ROOT}/scripts/validate-kv-config.shError 2: "429 Too Many Requests"
Diagnosis Flow: 1. Identify which operation caused 429 2. Check operation frequency 3. Analyze rate limit (1000/sec per key) 4. Review bulk operation usage
Solution Pattern:
Issue: Writing to same key >1000 times/second
Root Cause: Rate limit is 1000 writes/second PER KEY
Solutions:
1. Distribute writes across multiple keys:
await env.KV.put(`key:${Date.now()}`, value);
2. Add exponential backoff:
async function putWithRetry(key, value, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await env.KV.put(key, value);
} catch (err) {
if (err.message.includes('429') && i < retries - 1) {
await sleep(Math.pow(2, i) * 1000);
} else {
throw err;
}
}
}
}
3. Use waitUntil() to avoid blocking:
ctx.waitUntil(env.KV.put(key, value));Error 3: "Value is null (eventual consistency)"
Diagnosis Flow: 1. Verify write operation succeeded 2. Check timing (writes propagate in ~60s) 3. Determine if same-region or cross-region 4. Review cacheTtl usage
Solution Pattern:
Issue: Just wrote a value but get() returns null
Root Cause: Eventual consistency - writes take up to 60s to propagate globally
Solutions:
1. For immediate reads, use D1 (strong consistency):
- KV is optimized for read-heavy, eventually consistent data
- D1 is optimized for immediate consistency
2. Design for eventual consistency:
// Write with metadata timestamp
await env.KV.put('key', value, {
metadata: { updated: Date.now() }
});
// Read with fallback
let value = await env.KV.get('key');
if (!value) {
// Fallback logic or wait/retry
}
3. Use cacheTtl for consistent reads after initial propagation:
const value = await env.KV.get('key', { cacheTtl: 300 });Error 4: "env.MY_KV is undefined"
Diagnosis Flow: 1. Check TypeScript types defined 2. Verify binding in wrangler.jsonc 3. Confirm Worker parameter naming (env, ctx) 4. Test in wrangler dev vs production
Solution Pattern:
Issue: TypeScript shows env.MY_KV as undefined
Root Cause: Missing type definition
Fix:
1. Define Env interface:
type Env = {
MY_KV: KVNamespace;
};
2. Type Worker parameters:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const value = await env.MY_KV.get('key');
// env.MY_KV now has full type support
}
};
3. For Hono:
const app = new Hono<{ Bindings: Env }>();Error 5: "Operation timed out"
Diagnosis Flow: 1. Check operation type (get/put/list) 2. Verify value size (<25MB) 3. Review network connectivity 4. Check for concurrent operations
Solution Pattern:
Issue: KV operation times out
Common Causes:
1. Large value size (approaching 25MB limit)
2. Network issues
3. Too many concurrent operations (>1000/invocation limit)
Solutions:
1. Add timeout handling:
const timeout = (ms) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
try {
const value = await Promise.race([
env.KV.get('key'),
timeout(5000) // 5 second timeout
]);
} catch (err) {
if (err.message === 'Timeout') {
// Handle timeout
}
}
2. Check value size:
if (value.length > 20_000_000) { // 20MB
console.warn('Value approaching 25MB limit');
}
3. Use bulk operations for multiple reads:
// Instead of 100 separate get() calls (could hit limit)
const values = await env.KV.get(['key1', 'key2', ...]); // 1 operationExample Interactions
Example 1: Quick Error Fix
User: "I'm getting KV_ERROR when I try to put data"
Agent Actions: 1. Ask for error details and code 2. Read wrangler.jsonc 3. Run validation script 4. Identify issue: Missing namespace ID
Agent Response:
I found the issue. Your wrangler.jsonc has a malformed namespace configuration:
Current (broken):"kv_namespaces": [{ "binding": "MY_KV", "id": "" // ← Empty ID }]
Fix:
1. Get your namespace ID:
wrangler kv namespace list
2. Update wrangler.jsonc with the ID:
"id": "a1b2c3d4..."
3. Verify:
${CLAUDE_PLUGIN_ROOT}/scripts/validate-kv-config.sh
Would you like me to update the configuration for you?Example 2: Performance Debugging
User: "KV is slow, taking 500ms for each get()"
Agent Actions: 1. Check if cacheTtl is used 2. Review network/region setup 3. Test actual latency 4. Provide optimization
Agent Response:
The slow performance is because you're not using cacheTtl. Each get() hits the KV store directly:
Current performance:
- First read: ~50-200ms (varies by region)
- Subsequent reads: ~50-200ms (no caching)
With cacheTtl optimization:const value = await env.KV.get('config', { cacheTtl: 300 // Cache at edge for 5 min });
New performance:
- First read: ~50-200ms
- Cached reads: ~1-5ms (99% faster!)
For frequently-read data, this is a game-changer. Shall I add cacheTtl to your get() calls?Best Practices for Debugging
Do ✅
1. Check configuration first - Most errors are config issues 2. Run validation scripts - Automated checks catch common problems 3. Read error messages carefully - They often contain the solution 4. Test in isolation - Isolate the failing operation 5. Load troubleshooting.md - Comprehensive error catalog 6. Verify authentication - wrangler whoami to check login
Don't ❌
1. Assume the code is correct - Check config first 2. Skip validation - Always validate before debugging code 3. Ignore eventual consistency - Design for it, don't fight it 4. Debug without logs - Add console.log to understand flow 5. Fix without understanding - Know why the fix works
Reference Files Used
Load these references during debugging:
Primary:
references/troubleshooting.md- Complete error catalog with solutions
Supporting:
references/setup-guide.md- Configuration referencereferences/workers-api.md- API behavior and parametersreferences/limits-quotas.md- Rate limits and quotas
Success Metrics
Effective debugging achieves:
- Error resolution - Problem fixed
- Understanding - User knows why it failed
- Prevention - User can avoid future occurrences
- Monitoring - User can detect issues early
Related Commands
After debugging, recommend:
/test-kv MY_NAMESPACE - Verify the fix works
/optimize-kv - Check for performance issuesImplementation Notes
- Always validate configuration before debugging code
- Use scripts for automated diagnosis
- Provide clear, actionable solutions
- Explain root cause, not just the fix
- Test proposed solutions when possible
- Document the resolution for future reference
KV Optimizer Agent
Autonomous agent specialized in analyzing and optimizing Cloudflare Workers KV usage patterns for maximum performance and cost efficiency.
Agent Capabilities
Code Analysis
- Scans Worker files for KV operations
- Identifies missing TTL/expiration on put() calls
- Detects missing cacheTtl on get() operations
- Finds sequential operations that could be parallelized
- Identifies bulk operation opportunities
- Checks for proper error handling
- Analyzes waitUntil() usage patterns
Optimization Recommendations
- Prioritized list of improvements (critical → nice-to-have)
- Code examples for each optimization
- Before/after comparisons
- Estimated performance gains
- Cost savings calculations
- Risk assessment for each change
Automated Refactoring
- Applies optimizations to code
- Maintains functionality and tests
- Adds inline comments explaining changes
- Creates backup of original code
- Validates changes with testing
Performance Benchmarking
- Measures current performance metrics
- Estimates improvement impact
- Compares before/after results
- Generates performance reports
When to Use This Agent
The agent triggers when users mention:
- "optimize my kv usage"
- "improve kv performance"
- "reduce kv costs"
- "kv is slow"
- "make kv faster"
- "kv best practices"
- "review my kv code"
- "cacheTtl optimization"
- "bulk operations"
Agent Workflow
Phase 1: Analysis
1. Locate KV Code
- Use Glob to find Worker files
- Search for KV operations with Grep
- Identify all files using KV
2. Run Static Analysis
- Execute analyze-kv-usage.sh script:
${CLAUDE_PLUGIN_ROOT}/scripts/analyze-kv-usage.sh <file>- Parse output for issues and optimizations
- Categorize by severity
3. Load Reference Documentation
- Load
references/best-practices.mdfor optimization patterns - Load
references/performance-tuning.mdfor advanced techniques - Review current implementation against best practices
Phase 2: Recommendations
4. Generate Priority List
- Critical issues first (missing error handling)
- High-impact optimizations (missing cacheTtl)
- Cost-saving opportunities (missing TTL)
- Nice-to-have improvements (waitUntil usage)
5. Provide Examples
- Show current problematic code
- Provide optimized version
- Explain the benefit
- Estimate performance gain
6. Present to User
- Summarize findings
- Offer to apply optimizations automatically
- Or provide guidance for manual fixes
Phase 3: Implementation (If User Approves)
7. Apply Optimizations
- Read current code
- Apply transformations
- Preserve functionality
- Add explanatory comments
8. Validate Changes
- Check syntax
- Ensure no breaking changes
- Test if possible (use test-kv command)
9. Report Results
- List changes made
- Expected performance improvement
- Next steps for testing
Tools Available to Agent
- Read - Read Worker files and configuration
- Grep - Search for KV usage patterns across codebase
- Glob - Find Worker files
- Bash - Execute analyze-kv-usage.sh script
- Edit - Apply optimizations to code (with user approval)
- Write - Create optimization reports
Example Interactions
Example 1: Basic Optimization
User: "Optimize my KV usage in src/index.ts"
Agent Actions: 1. Read src/index.ts 2. Run analyze-kv-usage.sh script 3. Load best-practices.md 4. Identify issues:
- 8 put() calls without TTL
- 15 get() calls without cacheTtl
- No error handling
5. Present findings with code examples 6. Offer to apply fixes automatically
Agent Response:
I've analyzed your KV usage in src/index.ts and found several optimization opportunities:
Critical Issues (1):
❌ No error handling - All KV operations could crash Worker on failure
High-Impact Optimizations (2):
⚠️ Missing cacheTtl on 15 get() operations - Missing 50-90% performance gain
⚠️ Missing TTL on 8 put() operations - Unnecessary storage costs
I can apply these optimizations automatically. This will:
- Add try-catch error handling
- Add cacheTtl (300s) to frequently-read keys
- Add expirationTtl (1 hour) to session/cache data
- Maintain all existing functionality
Would you like me to proceed with these optimizations?Example 2: Performance Diagnosis
User: "My Worker is slow, I think it's the KV calls"
Agent Actions: 1. Search codebase for KV operations 2. Analyze patterns (sequential vs parallel) 3. Check for cacheTtl usage 4. Identify bottlenecks 5. Provide specific recommendations
Agent Response:
I found the performance issue. Your Worker is making 12 sequential KV get() calls:
// Current (slow): const user = await env.KV.get('user'); // ~50ms const prefs = await env.KV.get('prefs'); // ~50ms const stats = await env.KV.get('stats'); // ~50ms ... // Total: ~600ms
This can be parallelized:
// Optimized (fast): const [user, prefs, stats, ...] = await Promise.all([ env.KV.get('user'), env.KV.get('prefs'), env.KV.get('stats'), ... ]); // Total: ~50ms (12x faster!)
Additionally, adding cacheTtl:300 will cache these at the edge, reducing to ~1ms for cached hits.
Shall I apply these optimizations?Example 3: Cost Reduction
User: "How can I reduce my KV costs?"
Agent Actions: 1. Analyze put() operations for missing TTL 2. Check for unnecessary writes 3. Identify data that could use metadata instead 4. Calculate potential savings
Agent Response:
I found several cost optimization opportunities:
1. Missing TTL on 25 put() operations
- Current: Data persists forever
- Optimized: Add TTL for temporary data
- Savings: ~70% reduction in storage costs
2. Multiple small values (5 keys per user)
- Current: 5 separate KV entries
- Optimized: Coalesce into single JSON object
- Savings: 80% reduction in write operations
3. Analytics data using put()
- Current: Blocking writes for analytics
- Optimized: Use waitUntil() for async writes
- Benefit: Faster responses + same reliability
Estimated total savings: $150/month (based on current usage)
Would you like me to implement these optimizations?Best Practices Applied by Agent
Always Do ✅
1. Add TTL to temporary data - Prevent storage bloat 2. Use cacheTtl for frequently-read data - 50-90% faster reads 3. Parallelize independent operations - Use Promise.all() 4. Add error handling - Wrap KV calls in try-catch 5. Use waitUntil() for non-critical writes - Faster responses 6. Coalesce related keys - Reduce operation count 7. Add pagination to list() - Prevent hitting limits 8. Validate before optimizing - Test current behavior first
Never Do ❌
1. Remove error handling - Always maintain robustness 2. Change functionality without testing - Preserve behavior 3. Apply all optimizations blindly - Consider context 4. Ignore eventual consistency - Don't assume instant propagation 5. Over-optimize - Balance performance vs code complexity 6. Skip user approval - Get confirmation for significant changes
Reference Files Used
Load these references as needed during optimization:
For Analysis:
references/best-practices.md- Production patterns and anti-patternsreferences/performance-tuning.md- Advanced optimization techniques
For Troubleshooting:
references/troubleshooting.md- Common issues and solutions
For Validation:
references/workers-api.md- API reference and parameter options
Success Metrics
Track these outcomes:
- Performance: Response time reduction (%)
- Cost: Storage and operation savings ($)
- Reliability: Error rate reduction (%)
- Code Quality: Issues resolved count
Related Commands
After optimization, recommend:
/test-kv - Verify optimizations didn't break functionalityImplementation Notes
- Use Read tool to analyze code before modifying
- Execute analyze-kv-usage.sh for automated detection
- Load references when needed (don't load all upfront)
- Always get user approval before applying changes
- Test optimizations when possible
- Provide rollback instructions if needed
/cloudflare-kv:optimize - Analyze and Optimize KV Usage
This command analyzes Worker code for KV usage patterns and provides actionable optimization recommendations to improve performance and reduce costs.
What This Command Does
1. Analyzes Code Patterns
- Scans Worker files for KV operations
- Identifies get(), put(), delete(), list() calls
- Detects missing optimizations
2. Checks Best Practices
- TTL usage on put() operations
- cacheTtl usage on get() operations
- Error handling patterns
- Bulk operation opportunities
- waitUntil() for async writes
3. Generates Report
- Critical issues (must fix)
- Warnings (should fix)
- Optimizations (nice to have)
- Code examples for each issue
- Estimated cost/performance impact
How to Use
Analyze Single File
/cloudflare-kv:optimize src/index.tsAnalyze Multiple Files
Run multiple times for different files:
/cloudflare-kv:optimize src/index.ts
/cloudflare-kv:optimize src/api/routes.ts
/cloudflare-kv:optimize src/lib/kv-utils.tsInteractive Mode
If no file specified, command will: 1. Search for common Worker files (src/index.ts, index.js, worker.ts) 2. List found files 3. Ask which to analyze
/cloudflare-kv:optimizeImplementation
Execute the analysis script from the cloudflare-kv skill:
${CLAUDE_PLUGIN_ROOT}/scripts/analyze-kv-usage.sh <worker-file>The script performs static code analysis to detect:
- Missing TTL/expiration on put()
- Missing cacheTtl on get()
- Lack of error handling
- Sequential operations that could be parallel
- Missing pagination on list()
- Opportunities for waitUntil()
- JSON.stringify usage for objects
Example Output
Cloudflare Workers KV - Usage Analyzer
======================================
Analyzing: src/index.ts
KV Operations Found:
- get(): 15
- put(): 8
- delete(): 3
- list(): 2
Issue Check 1: Missing TTL on put() operations
----------------------------------------------
⚠ 5 put() operation(s) without TTL/expiration
Issue: Data will persist indefinitely, increasing storage costs
Fix: Add expirationTtl or expiration to put() calls
Example:
await env.KV.put('key', 'value', { expirationTtl: 3600 });
Issue Check 2: Missing cacheTtl on get() operations
---------------------------------------------------
⚠ 12 get() operation(s) without cacheTtl
Issue: Missing edge caching optimization
Fix: Add cacheTtl for frequently-read data (min 60 seconds)
Example:
const value = await env.KV.get('key', { cacheTtl: 300 });
Issue Check 3: Missing error handling
-------------------------------------
✗ No try-catch blocks found
Issue: KV operations can fail (rate limits, network errors)
Fix: Wrap KV operations in try-catch
Example:
try {
const value = await env.KV.get('key');
} catch (error) {
console.error('KV error:', error);
// Handle gracefully
}
Issue Check 4: Sequential get() calls (bulk read opportunity)
-------------------------------------------------------------
⚠ Multiple sequential await get() calls detected
Issue: Each get() counts as separate operation
Fix: Consider using Promise.all() for parallel reads
Example:
const [val1, val2, val3] = await Promise.all([
env.KV.get('key1'),
env.KV.get('key2'),
env.KV.get('key3')
]);
========================================
Summary
========================================
Critical Issues: 1
Warnings: 3
Optimizations: 2
⚠ Critical issues found
Please address critical issues before deploying to production.
For more details, see:
- references/best-practices.md
- references/performance-tuning.mdOptimization Categories
Critical Issues (Must Fix)
These can cause runtime errors or data loss:
❌ No error handling
- Impact: Worker crashes on KV errors
- Fix: Add try-catch blocks
- Priority: HIGH
Warnings (Should Fix)
These affect reliability and costs:
⚠️ Missing TTL on put()
- Impact: Unnecessary storage costs
- Fix: Add expirationTtl
- Savings: Significant (depends on data volume)
⚠️ Missing pagination on list()
- Impact: Could hit 1000 key limit
- Fix: Add limit parameter
- Priority: MEDIUM
Optimizations (Nice to Have)
These improve performance:
💡 Missing cacheTtl on get()
- Impact: Slower reads, higher latency
- Fix: Add cacheTtl parameter
- Improvement: 50-90% faster reads
💡 Sequential operations
- Impact: Slower execution
- Fix: Use Promise.all()
- Improvement: 2-5x faster
💡 No waitUntil() usage
- Impact: Slower response times
- Fix: Use ctx.waitUntil() for non-critical writes
- Improvement: 10-100ms faster responses
After Analysis
Based on the report:
If Critical Issues Found
1. Fix Immediately
- Add error handling
- Test thoroughly
- Don't deploy without fixing
2. Reference Documentation
- Load
references/best-practices.mdfor error handling patterns - Load
references/troubleshooting.mdfor error recovery
If Warnings Found
1. Prioritize Fixes
- Address high-cost warnings first (missing TTL)
- Plan fixes for next development cycle
2. Estimate Impact
- Calculate storage cost savings
- Measure performance improvements
If Only Optimizations
1. Implement Gradually
- Start with highest-impact optimizations
- Measure before/after performance
- Document improvements
2. Benchmark
# Before optimization
wrangler dev
# Test response times
# After optimization
wrangler dev
# Compare response timesOptimization Examples
Before: Missing TTL
// ❌ Data persists forever
await env.KV.put('session', sessionData);After: With TTL
// ✅ Auto-expires after 1 hour
await env.KV.put('session', sessionData, {
expirationTtl: 3600
});Impact: Prevents storage bloat, reduces costs
---
Before: Sequential Reads
// ❌ Each await blocks execution
const user = await env.KV.get('user:123');
const prefs = await env.KV.get('prefs:123');
const stats = await env.KV.get('stats:123');After: Parallel Reads
// ✅ All reads happen simultaneously
const [user, prefs, stats] = await Promise.all([
env.KV.get('user:123'),
env.KV.get('prefs:123'),
env.KV.get('stats:123')
]);Impact: 3x faster execution
---
Before: Blocking Writes
// ❌ Response waits for write
await env.KV.put('analytics', data);
return new Response('OK');After: Non-blocking Writes
// ✅ Response returns immediately
ctx.waitUntil(
env.KV.put('analytics', data)
);
return new Response('OK');Impact: 50-100ms faster responses
Automated Optimization
For complex codebases, consider using the kv-optimizer agent for automated refactoring:
@kv-optimizer Please optimize my KV usage in src/index.tsThe agent will:
- Apply optimizations automatically
- Maintain code functionality
- Add tests for changes
- Provide before/after metrics
Related Commands
/cloudflare-kv:test- Test KV operations after optimization/cloudflare-kv:setup- Configure new namespaces
References
For comprehensive optimization guidance:
- Load
references/best-practices.mdfor production patterns - Load
references/performance-tuning.mdfor advanced optimizations - Check official docs: https://developers.cloudflare.com/kv/best-practices/
/cloudflare-kv:setup - Interactive KV Namespace Setup
This command guides through the complete setup process for Cloudflare Workers KV namespaces.
What This Command Does
1. Creates KV Namespaces
- Production namespace
- Preview namespace (for testing)
- Generates unique IDs for both
2. Configures wrangler.jsonc
- Adds KV namespace bindings
- Sets up preview environment
- Creates config if needed
3. Generates TypeScript Types
- Env interface with KV bindings
- Type-safe Worker code examples
4. Tests Connection
- Validates configuration
- Performs basic CRUD operations
- Verifies namespace accessibility
How to Use
Interactive Mode (Recommended)
Run the command and follow prompts:
/cloudflare-kv:setupThe command will: 1. Ask for namespace name (e.g., "MY_KV" or "USER_DATA") 2. Create production and preview namespaces 3. Generate wrangler.jsonc configuration 4. Provide TypeScript setup examples 5. Offer to run connection tests
With Namespace Name
Provide namespace name upfront:
/cloudflare-kv:setup MY_NAMESPACEImplementation
Execute the setup script from the cloudflare-kv skill:
${CLAUDE_PLUGIN_ROOT}/scripts/setup-kv-namespace.sh [namespace-name]The script will:
- Check wrangler CLI installation
- Verify authentication status
- Create namespaces via wrangler API
- Extract namespace IDs
- Generate configuration snippets
- Optionally create/update wrangler.jsonc
Prerequisites
Before running this command, ensure:
1. Wrangler CLI Installed
npm install -g wrangler2. Authenticated with Cloudflare
wrangler login3. Active Cloudflare Account
- Workers plan enabled
- API access configured
Example Output
Cloudflare Workers KV - Namespace Setup Wizard
================================================
Creating KV namespaces for: MY_KV_NAMESPACE
Step 1: Creating production namespace...
✓ Production namespace created
ID: a1b2c3d4e5f6789012345678901234ab
Step 2: Creating preview namespace...
✓ Preview namespace created
ID: b2c3d4e5f6789012345678901234abc1
✓ Namespaces created successfully!
Step 3: Generating wrangler.jsonc configuration...
Add this to your wrangler.jsonc:
"kv_namespaces": [
{
"binding": "MY_KV_NAMESPACE",
"id": "a1b2c3d4e5f6789012345678901234ab",
"preview_id": "b2c3d4e5f6789012345678901234abc1"
}
]
Step 4: TypeScript type definition example...
Add this to your Worker code for TypeScript support:
type Env = {
MY_KV_NAMESPACE: KVNamespace;
};
✓ Setup complete!
Next steps:
1. Ensure wrangler.jsonc has the KV namespace configuration
2. Use env.MY_KV_NAMESPACE in your Worker code
3. Test with: wrangler devAfter Setup
Once setup completes:
1. Verify Configuration
${CLAUDE_PLUGIN_ROOT}/scripts/validate-kv-config.sh2. Test Connection
/cloudflare-kv:test MY_KV_NAMESPACE3. Start Developing
export default {
async fetch(request, env: Env) {
await env.MY_KV_NAMESPACE.put('key', 'value');
const value = await env.MY_KV_NAMESPACE.get('key');
return new Response(value);
}
};4. Run Locally
wrangler devCommon Issues
"Not logged in to Wrangler"
Solution: Authenticate first
wrangler login"Failed to create namespace"
Possible causes:
- No Workers plan enabled
- API token lacks permissions
- Account quota exceeded
Solution: Check Cloudflare dashboard and account settings
"wrangler.jsonc exists"
The script won't overwrite existing configs. You'll need to manually add the KV namespace configuration shown in the output.
Related Commands
/cloudflare-kv:test- Test KV operations and connection/cloudflare-kv:optimize- Analyze KV usage patterns
References
For more details:
- Load
references/setup-guide.mdfor complete setup documentation - Load
references/best-practices.mdfor production configuration - Check official docs: https://developers.cloudflare.com/kv/get-started/
/cloudflare-kv:test - Test KV Namespace Connection
This command validates KV namespace configuration and tests basic operations to ensure everything is working correctly.
What This Command Does
1. Validates Configuration
- Checks wrangler.jsonc for KV bindings
- Verifies namespace ID format
- Confirms binding name correctness
2. Tests CRUD Operations
- PUT: Creates test key-value pair
- GET: Retrieves and validates value
- DELETE: Removes test key
- Verifies cleanup after deletion
3. Reports Diagnostics
- Operation success/failure status
- Error messages with troubleshooting hints
- Configuration recommendations
How to Use
Basic Usage
Test a specific namespace binding:
/cloudflare-kv:test MY_NAMESPACEReplace MY_NAMESPACE with your actual binding name from wrangler.jsonc.
Interactive Mode
If no namespace provided, the command will: 1. List available namespaces from wrangler.jsonc 2. Ask which one to test 3. Run comprehensive tests
/cloudflare-kv:testImplementation
Execute the test script from the cloudflare-kv skill:
${CLAUDE_PLUGIN_ROOT}/scripts/test-kv-connection.sh <NAMESPACE_BINDING>The script performs: 1. Configuration validation 2. Test key generation (timestamped, unique) 3. PUT operation test 4. GET operation and value verification 5. DELETE operation test 6. Cleanup verification
Prerequisites
1. Wrangler CLI Installed
npm install -g wrangler2. Authenticated
wrangler whoami # Verify authentication3. Namespace Configured
- wrangler.jsonc must have kv_namespaces section
- Namespace ID must be valid
Example Output
Successful Test
Cloudflare Workers KV - Connection Tester
==========================================
Testing namespace: MY_KV_NAMESPACE
Step 1: Checking namespace configuration...
✓ Namespace binding found in wrangler.jsonc
✓ Namespace ID: a1b2c3d4e5f6789012345678901234ab
Step 2: Testing KV operations...
Test key: __test_connection_1735300800
Testing PUT operation... ✓ Success
Testing GET operation... ✓ Success (value matches)
Testing DELETE operation... ✓ Success
Verifying deletion... ✓ Success (key removed)
Summary:
========
✓ All tests passed!
Your KV namespace 'MY_KV_NAMESPACE' is working correctly.Failed Test
Testing namespace: BROKEN_NAMESPACE
Step 1: Checking namespace configuration...
✗ Namespace binding 'BROKEN_NAMESPACE' not found in wrangler.jsonc
Add it to your wrangler.jsonc:
"kv_namespaces": [
{
"binding": "BROKEN_NAMESPACE",
"id": "your-namespace-id"
}
]Test Scope
This command tests:
✅ Configuration
- Namespace binding exists
- Namespace ID is valid format (32 hex chars)
- wrangler.jsonc syntax is correct
✅ Operations
- PUT: Can write key-value pairs
- GET: Can retrieve values
- DELETE: Can remove keys
- Values match after write/read cycle
❌ Not Tested
- TTL/expiration (requires time delay)
- Metadata operations
- List operations
- Rate limits
- Eventual consistency across regions
For comprehensive testing, see references/troubleshooting.md.
Common Test Failures
"Namespace binding not found"
Cause: Missing or incorrect configuration
Solution: 1. Run /cloudflare-kv:setup to create namespace 2. Or manually add to wrangler.jsonc:
"kv_namespaces": [{
"binding": "MY_NAMESPACE",
"id": "your-id"
}]"PUT operation failed"
Possible causes:
- Invalid namespace ID
- Authentication expired
- API rate limit reached
- Network connectivity issue
Solution:
# Re-authenticate
wrangler login
# Verify namespace ID is correct
wrangler kv namespace list
# Check wrangler.jsonc configuration
${CLAUDE_PLUGIN_ROOT}/scripts/validate-kv-config.sh"GET value mismatch"
Cause: Eventual consistency delay
Solution: This is rare in local testing. If persistent: 1. Wait 60 seconds and retry 2. Check if namespace ID is correct 3. Verify no other processes are modifying the test key
After Testing
If tests pass:
1. Start Development
wrangler dev2. Optimize Usage
/cloudflare-kv:optimize src/index.ts3. Deploy to Production
wrangler deployIf tests fail:
1. Validate Configuration
${CLAUDE_PLUGIN_ROOT}/scripts/validate-kv-config.sh2. Check Troubleshooting Guide
- Load
references/troubleshooting.mdfor detailed error solutions
3. Verify Account Status
- Check Cloudflare dashboard
- Ensure Workers plan is active
- Verify API permissions
Related Commands
/cloudflare-kv:setup- Create and configure new namespace/cloudflare-kv:optimize- Analyze KV usage patterns
References
For more details:
- Load
references/troubleshooting.mdfor error diagnostics - Load
references/setup-guide.mdfor configuration help - Check official docs: https://developers.cloudflare.com/kv/api/
/**
* Cloudflare Workers KV - API Response Caching Example
*
* Demonstrates HTTP caching patterns using KV:
* - Basic cache-aside pattern
* - Stale-while-revalidate
* - Cache invalidation
* - Conditional caching
*/
import { Hono } from 'hono';
type Bindings = {
API_CACHE: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Pattern 1: Basic Cache-Aside
// ============================================================================
app.get('/api/posts/:id', async (c) => {
const postId = c.req.param('id');
const cacheKey = `post:${postId}`;
// Try cache first
let post = await c.env.API_CACHE.get(cacheKey, 'json');
if (!post) {
// Cache miss - fetch from origin
console.log(`Cache MISS for ${cacheKey}`);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
post = await response.json();
// Store in cache with 5-minute TTL
await c.env.API_CACHE.put(cacheKey, JSON.stringify(post), {
expirationTtl: 300
});
} else {
console.log(`Cache HIT for ${cacheKey}`);
}
return c.json(post);
});
// ============================================================================
// Pattern 2: Stale-While-Revalidate
// ============================================================================
app.get('/api/users/:id', async (c) => {
const userId = c.req.param('id');
const cacheKey = `user:${userId}`;
// Get cached value with metadata
const { value, metadata } = await c.env.API_CACHE.getWithMetadata(cacheKey, 'json');
const cachedAt = metadata?.cachedAt as number || 0;
const age = Date.now() - cachedAt;
// Serve stale content if available (< 1 hour old)
if (value && age < 3600000) {
console.log(`Serving cached user (age: ${Math.floor(age / 1000)}s)`);
// If content is getting old (> 5 minutes), revalidate in background
if (age > 300000) {
console.log('Background revalidation triggered');
c.executionCtx.waitUntil(
(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const fresh = await response.json();
await c.env.API_CACHE.put(cacheKey, JSON.stringify(fresh), {
metadata: { cachedAt: Date.now() }
});
console.log('Cache revalidated');
})()
);
}
return c.json(value);
}
// No cache or too old - fetch fresh
console.log('Fetching fresh user data');
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const user = await response.json();
await c.env.API_CACHE.put(cacheKey, JSON.stringify(user), {
metadata: { cachedAt: Date.now() }
});
return c.json(user);
});
// ============================================================================
// Pattern 3: Conditional Caching
// ============================================================================
app.get('/api/comments', async (c) => {
const postId = c.req.query('postId');
const cacheKey = `comments:${postId || 'all'}`;
// Only cache if specific postId
if (!postId) {
console.log('Not caching - no postId filter');
const response = await fetch('https://jsonplaceholder.typicode.com/comments');
return c.json(await response.json());
}
// Cache for specific postId
let comments = await c.env.API_CACHE.get(cacheKey, 'json');
if (!comments) {
console.log(`Cache MISS for ${cacheKey}`);
const response = await fetch(
`https://jsonplaceholder.typicode.com/comments?postId=${postId}`
);
comments = await response.json();
await c.env.API_CACHE.put(cacheKey, JSON.stringify(comments), {
expirationTtl: 600 // 10 minutes
});
}
return c.json(comments);
});
// ============================================================================
// Pattern 4: Cache with ETag
// ============================================================================
app.get('/api/photos/:id', async (c) => {
const photoId = c.req.param('id');
const cacheKey = `photo:${photoId}`;
const clientEtag = c.req.header('if-none-match');
const { value, metadata } = await c.env.API_CACHE.getWithMetadata(cacheKey, 'json');
const etag = metadata?.etag as string;
// Client has current version
if (clientEtag && clientEtag === etag) {
return c.body(null, 304); // Not Modified
}
let photo = value;
if (!photo) {
// Fetch from origin
const response = await fetch(
`https://jsonplaceholder.typicode.com/photos/${photoId}`
);
photo = await response.json();
// Generate ETag from content hash
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(photo));
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const newEtag = hashArray.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 16);
await c.env.API_CACHE.put(cacheKey, JSON.stringify(photo), {
expirationTtl: 3600,
metadata: { etag: newEtag }
});
}
return c.json(photo, 200, {
'ETag': etag || 'unknown',
'Cache-Control': 'max-age=3600'
});
});
// ============================================================================
// Cache Management
// ============================================================================
/**
* Invalidate specific cache entry
*/
app.delete('/cache/:type/:id', async (c) => {
const type = c.req.param('type');
const id = c.req.param('id');
const cacheKey = `${type}:${id}`;
await c.env.API_CACHE.delete(cacheKey);
return c.json({ success: true, invalidated: cacheKey });
});
/**
* Invalidate all cache for a type
*/
app.delete('/cache/:type', async (c) => {
const type = c.req.param('type');
const { keys } = await c.env.API_CACHE.list({
prefix: `${type}:`,
limit: 1000
});
await Promise.all(
keys.map(({ name }) => c.env.API_CACHE.delete(name))
);
return c.json({ success: true, invalidated: keys.length });
});
/**
* Get cache statistics
*/
app.get('/cache/stats', async (c) => {
const types = ['post', 'user', 'comments', 'photo'];
const stats = await Promise.all(
types.map(async (type) => {
const { keys } = await c.env.API_CACHE.list({
prefix: `${type}:`,
limit: 1000
});
return { type, cached: keys.length };
})
);
return c.json({ stats });
});
// ============================================================================
// Root & 404
// ============================================================================
app.get('/', (c) => {
return c.html(`
<h1>Cloudflare Workers KV - API Caching Example</h1>
<h2>Cached Endpoints</h2>
<ul>
<li><a href="/api/posts/1">/api/posts/:id</a> - Basic cache-aside (5 min TTL)</li>
<li><a href="/api/users/1">/api/users/:id</a> - Stale-while-revalidate</li>
<li><a href="/api/comments?postId=1">/api/comments?postId=1</a> - Conditional caching</li>
<li><a href="/api/photos/1">/api/photos/:id</a> - Cache with ETag</li>
</ul>
<h2>Cache Management</h2>
<ul>
<li>DELETE /cache/:type/:id - Invalidate specific entry</li>
<li>DELETE /cache/:type - Invalidate all entries of type</li>
<li><a href="/cache/stats">GET /cache/stats</a> - View cache statistics</li>
</ul>
`);
});
app.notFound((c) => c.json({ error: 'Not found' }, 404));
export default app;
{
"name": "kv-api-caching-example",
"main": "index.ts",
"compatibility_date": "2025-10-11",
"compatibility_flags": ["nodejs_compat"],
"kv_namespaces": [
{
"binding": "API_CACHE",
"id": "YOUR_PRODUCTION_NAMESPACE_ID",
"preview_id": "YOUR_PREVIEW_NAMESPACE_ID"
}
],
"dev": {
"port": 8787
}
}
/*
* Setup:
* 1. bunx wrangler kv namespace create API_CACHE
* 2. bunx wrangler kv namespace create API_CACHE --preview
* 3. Update IDs above
* 4. bun add hono && bun add -D @cloudflare/workers-types
* 5. bunx wrangler dev
*/
/**
* Cloudflare Workers KV - Configuration Management Example
*
* Demonstrates config management patterns using KV:
* - Feature flags
* - Environment-specific configs
* - Hot-reload without Worker redeployment
* - Version tracking
* - A/B testing configuration
*/
import { Hono } from 'hono';
type Bindings = {
CONFIG: KVNamespace;
};
interface FeatureFlags {
darkMode: boolean;
newUI: boolean;
betaFeatures: boolean;
maintenanceMode: boolean;
}
interface AppConfig {
apiUrl: string;
maxUploadSize: number;
environment: 'development' | 'staging' | 'production';
features: FeatureFlags;
version: string;
}
const app = new Hono<{ Bindings: Bindings }>();
// Default configuration (fallback)
const DEFAULT_CONFIG: AppConfig = {
apiUrl: 'https://api.example.com',
maxUploadSize: 10485760, // 10MB
environment: 'production',
features: {
darkMode: true,
newUI: false,
betaFeatures: false,
maintenanceMode: false
},
version: '1.0.0'
};
// ============================================================================
// Configuration Helpers
// ============================================================================
async function getConfig(kv: KVNamespace): Promise<AppConfig> {
const config = await kv.get('app:config', {
type: 'json',
cacheTtl: 60 // Cache for 1 minute
});
return (config as AppConfig) || DEFAULT_CONFIG;
}
async function setConfig(kv: KVNamespace, config: AppConfig): Promise<void> {
await kv.put('app:config', JSON.stringify(config), {
metadata: {
updatedAt: Date.now(),
version: config.version
}
});
}
async function getFeatureFlag(kv: KVNamespace, flag: string): Promise<boolean> {
const config = await getConfig(kv);
return config.features[flag as keyof FeatureFlags] || false;
}
// ============================================================================
// Configuration Endpoints
// ============================================================================
/**
* Get current configuration
*/
app.get('/config', async (c) => {
const config = await getConfig(c.env.CONFIG);
return c.json(config);
});
/**
* Update configuration (admin only)
*/
app.put('/config', async (c) => {
const updates = await c.req.json();
const current = await getConfig(c.env.CONFIG);
// Merge updates
const newConfig: AppConfig = {
...current,
...updates,
version: `${parseInt(current.version) + 1}.0.0`
};
await setConfig(c.env.CONFIG, newConfig);
return c.json({ success: true, config: newConfig });
});
/**
* Get specific feature flag
*/
app.get('/config/features/:flag', async (c) => {
const flag = c.req.param('flag');
const enabled = await getFeatureFlag(c.env.CONFIG, flag);
return c.json({ flag, enabled });
});
/**
* Toggle feature flag (admin only)
*/
app.post('/config/features/:flag/toggle', async (c) => {
const flag = c.req.param('flag');
const config = await getConfig(c.env.CONFIG);
if (!(flag in config.features)) {
return c.json({ error: 'Unknown feature flag' }, 400);
}
// Toggle flag
config.features[flag as keyof FeatureFlags] = !config.features[flag as keyof FeatureFlags];
config.version = `${parseInt(config.version) + 1}.0.0`;
await setConfig(c.env.CONFIG, config);
return c.json({
success: true,
flag,
enabled: config.features[flag as keyof FeatureFlags]
});
});
// ============================================================================
// A/B Testing Configuration
// ============================================================================
interface ABTest {
name: string;
variants: string[];
distribution: number[]; // Percentages for each variant
enabled: boolean;
}
/**
* Get A/B test variant for user
*/
app.get('/ab/:testName', async (c) => {
const testName = c.req.param('testName');
const userId = c.req.query('userId') || 'anonymous';
// Get test configuration
const testConfig = await c.env.CONFIG.get(`ab:${testName}`, 'json') as ABTest | null;
if (!testConfig || !testConfig.enabled) {
return c.json({ variant: 'control' });
}
// Deterministic variant assignment based on userId
const encoder = new TextEncoder();
const data = encoder.encode(`${testName}:${userId}`);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = new Uint8Array(hashBuffer);
const hash = hashArray[0]; // Use first byte
// Map hash to variant
const percentage = (hash / 255) * 100;
let cumulative = 0;
for (let i = 0; i < testConfig.variants.length; i++) {
cumulative += testConfig.distribution[i];
if (percentage < cumulative) {
return c.json({
test: testName,
variant: testConfig.variants[i],
userId
});
}
}
return c.json({ variant: testConfig.variants[testConfig.variants.length - 1] });
});
/**
* Create/update A/B test (admin only)
*/
app.put('/ab/:testName', async (c) => {
const testName = c.req.param('testName');
const test: ABTest = await c.req.json();
// Validate distribution sums to 100
const sum = test.distribution.reduce((a, b) => a + b, 0);
if (Math.abs(sum - 100) > 0.01) {
return c.json({ error: 'Distribution must sum to 100%' }, 400);
}
await c.env.CONFIG.put(`ab:${testName}`, JSON.stringify(test), {
metadata: { createdAt: Date.now() }
});
return c.json({ success: true, test });
});
// ============================================================================
// Environment-Specific Configuration
// ============================================================================
/**
* Get environment-specific setting
*/
app.get('/config/env/:key', async (c) => {
const key = c.req.param('key');
const config = await getConfig(c.env.CONFIG);
// Get environment-specific override
const envKey = `env:${config.environment}:${key}`;
const envValue = await c.env.CONFIG.get(envKey);
if (envValue) {
return c.json({ key, value: envValue, source: 'environment' });
}
// Fallback to global config
const globalValue = (config as any)[key];
return c.json({ key, value: globalValue, source: 'global' });
});
// ============================================================================
// Configuration History & Rollback
// ============================================================================
/**
* Save configuration snapshot
*/
app.post('/config/snapshot', async (c) => {
const config = await getConfig(c.env.CONFIG);
const timestamp = Date.now();
await c.env.CONFIG.put(
`config:snapshot:${timestamp}`,
JSON.stringify(config),
{
metadata: { version: config.version, timestamp },
expirationTtl: 86400 * 30 // Keep for 30 days
}
);
return c.json({ success: true, timestamp, version: config.version });
});
/**
* List configuration snapshots
*/
app.get('/config/snapshots', async (c) => {
const { keys } = await c.env.CONFIG.list({
prefix: 'config:snapshot:',
limit: 50
});
const snapshots = keys.map(({ name, metadata }) => ({
timestamp: parseInt(name.replace('config:snapshot:', '')),
version: metadata?.version,
date: new Date(metadata?.timestamp as number).toISOString()
}));
return c.json({ snapshots });
});
/**
* Rollback to snapshot
*/
app.post('/config/rollback/:timestamp', async (c) => {
const timestamp = c.req.param('timestamp');
const snapshot = await c.env.CONFIG.get(`config:snapshot:${timestamp}`, 'json') as AppConfig;
if (!snapshot) {
return c.json({ error: 'Snapshot not found' }, 404);
}
await setConfig(c.env.CONFIG, snapshot);
return c.json({ success: true, rolledBackTo: timestamp });
});
// ============================================================================
// Application Endpoints (Using Configuration)
// ============================================================================
/**
* Example endpoint that uses feature flags
*/
app.get('/app/dashboard', async (c) => {
const config = await getConfig(c.env.CONFIG);
if (config.features.maintenanceMode) {
return c.json({ error: 'Service under maintenance' }, 503);
}
return c.json({
message: 'Welcome to dashboard',
ui: config.features.newUI ? 'new' : 'classic',
darkMode: config.features.darkMode
});
});
// ============================================================================
// Root & 404
// ============================================================================
app.get('/', (c) => {
return c.html(`
<h1>Cloudflare Workers KV - Config Management Example</h1>
<h2>Configuration</h2>
<ul>
<li><a href="/config">GET /config</a> - Get current configuration</li>
<li>PUT /config - Update configuration</li>
<li>GET /config/features/:flag - Get feature flag status</li>
<li>POST /config/features/:flag/toggle - Toggle feature flag</li>
</ul>
<h2>A/B Testing</h2>
<ul>
<li>GET /ab/:testName?userId=123 - Get A/B test variant</li>
<li>PUT /ab/:testName - Create/update A/B test</li>
</ul>
<h2>History & Rollback</h2>
<ul>
<li>POST /config/snapshot - Create configuration snapshot</li>
<li><a href="/config/snapshots">GET /config/snapshots</a> - List snapshots</li>
<li>POST /config/rollback/:timestamp - Rollback to snapshot</li>
</ul>
`);
});
app.notFound((c) => c.json({ error: 'Not found' }, 404));
export default app;
{
"name": "kv-config-management-example",
"main": "index.ts",
"compatibility_date": "2025-10-11",
"compatibility_flags": ["nodejs_compat"],
"kv_namespaces": [
{
"binding": "CONFIG",
"id": "YOUR_PRODUCTION_NAMESPACE_ID",
"preview_id": "YOUR_PREVIEW_NAMESPACE_ID"
}
],
"dev": {
"port": 8787
}
}
/*
* Setup:
* 1. bunx wrangler kv namespace create CONFIG
* 2. bunx wrangler kv namespace create CONFIG --preview
* 3. Update IDs above
* 4. bun add hono && bun add -D @cloudflare/workers-types
* 5. bunx wrangler dev
*/
/**
* Cloudflare Workers KV - Rate Limiting Example
*
* Demonstrates multiple rate limiting strategies using KV:
* - Fixed Window Rate Limiting
* - Sliding Window Rate Limiting
* - Token Bucket Rate Limiting
*
* Production-ready Worker with Hono framework
*/
import { Hono } from 'hono';
type Bindings = {
RATE_LIMIT: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// ============================================================================
// Strategy 1: Fixed Window Rate Limiting (Simple)
// ============================================================================
/**
* Fixed window: 100 requests per minute per IP
* Pros: Simple, low KV operations
* Cons: Burst traffic at window boundaries
*/
app.get('/api/fixed-window', async (c) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const minute = Math.floor(Date.now() / 60000); // Current minute
const key = `fixed:${ip}:${minute}`;
// Get current count
const countStr = await c.env.RATE_LIMIT.get(key);
const count = countStr ? parseInt(countStr) : 0;
// Check limit
const limit = 100;
if (count >= limit) {
return c.json(
{
error: 'Rate limit exceeded',
limit,
reset: (minute + 1) * 60000
},
429
);
}
// Increment counter
await c.env.RATE_LIMIT.put(key, String(count + 1), {
expirationTtl: 120 // 2 minutes (buffer for safety)
});
return c.json({
success: true,
remaining: limit - count - 1,
reset: (minute + 1) * 60000
});
});
// ============================================================================
// Strategy 2: Sliding Window Rate Limiting (Better)
// ============================================================================
/**
* Sliding window: 100 requests per minute (rolling)
* Pros: Smoother rate limiting, no burst issue
* Cons: More complex, more KV operations
*/
app.get('/api/sliding-window', async (c) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const now = Date.now();
const windowMs = 60000; // 1 minute
const limit = 100;
// Key for timestamp log
const key = `sliding:${ip}`;
// Get existing timestamps
const logStr = await c.env.RATE_LIMIT.get(key);
const timestamps: number[] = logStr ? JSON.parse(logStr) : [];
// Remove timestamps older than window
const validTimestamps = timestamps.filter(ts => now - ts < windowMs);
// Check limit
if (validTimestamps.length >= limit) {
const oldestTimestamp = Math.min(...validTimestamps);
const resetTime = oldestTimestamp + windowMs;
return c.json(
{
error: 'Rate limit exceeded',
limit,
reset: resetTime
},
429
);
}
// Add current timestamp
validTimestamps.push(now);
// Store updated log
await c.env.RATE_LIMIT.put(key, JSON.stringify(validTimestamps), {
expirationTtl: Math.ceil(windowMs / 1000) + 60 // Window + buffer
});
return c.json({
success: true,
remaining: limit - validTimestamps.length,
reset: now + windowMs
});
});
// ============================================================================
// Strategy 3: Token Bucket Rate Limiting (Advanced)
// ============================================================================
interface TokenBucket {
tokens: number;
lastRefill: number;
}
/**
* Token bucket: Allows bursts, refills over time
* Pros: Flexible, allows controlled bursts
* Cons: Most complex
*/
app.get('/api/token-bucket', async (c) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const key = `bucket:${ip}`;
const maxTokens = 100;
const refillRate = 100 / 60; // 100 tokens per minute = ~1.67/sec
// Get current bucket state
const bucketStr = await c.env.RATE_LIMIT.get(key);
let bucket: TokenBucket;
if (!bucketStr) {
// Initialize new bucket
bucket = {
tokens: maxTokens,
lastRefill: Date.now()
};
} else {
bucket = JSON.parse(bucketStr);
// Refill tokens based on time elapsed
const now = Date.now();
const elapsedSeconds = (now - bucket.lastRefill) / 1000;
const tokensToAdd = elapsedSeconds * refillRate;
bucket.tokens = Math.min(maxTokens, bucket.tokens + tokensToAdd);
bucket.lastRefill = now;
}
// Check if enough tokens
if (bucket.tokens < 1) {
const timeUntilToken = (1 - bucket.tokens) / refillRate;
return c.json(
{
error: 'Rate limit exceeded',
reset: Date.now() + timeUntilToken * 1000
},
429
);
}
// Consume 1 token
bucket.tokens -= 1;
// Save bucket state
await c.env.RATE_LIMIT.put(key, JSON.stringify(bucket), {
expirationTtl: 3600 // 1 hour
});
return c.json({
success: true,
tokensRemaining: Math.floor(bucket.tokens)
});
});
// ============================================================================
// Strategy 4: Multi-Tier Rate Limiting
// ============================================================================
/**
* Different limits for different user tiers
*/
app.get('/api/multi-tier', async (c) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const apiKey = c.req.header('x-api-key');
// Determine user tier (in production, look up in database)
let tier: 'free' | 'pro' | 'enterprise';
let limit: number;
if (!apiKey) {
tier = 'free';
limit = 10; // 10 requests/minute
} else if (apiKey.startsWith('pro_')) {
tier = 'pro';
limit = 100; // 100 requests/minute
} else {
tier = 'enterprise';
limit = 1000; // 1000 requests/minute
}
const minute = Math.floor(Date.now() / 60000);
const key = `tier:${tier}:${ip}:${minute}`;
const countStr = await c.env.RATE_LIMIT.get(key);
const count = countStr ? parseInt(countStr) : 0;
if (count >= limit) {
return c.json(
{
error: 'Rate limit exceeded',
tier,
limit,
reset: (minute + 1) * 60000
},
429
);
}
await c.env.RATE_LIMIT.put(key, String(count + 1), {
expirationTtl: 120
});
return c.json({
success: true,
tier,
remaining: limit - count - 1,
reset: (minute + 1) * 60000
});
});
// ============================================================================
// Utility: Check Current Rate Limit Status
// ============================================================================
app.get('/api/status', async (c) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
const minute = Math.floor(Date.now() / 60000);
// Check all rate limit strategies
const [fixed, sliding, bucket, tier] = await Promise.all([
// Fixed window
c.env.RATE_LIMIT.get(`fixed:${ip}:${minute}`),
// Sliding window
c.env.RATE_LIMIT.get(`sliding:${ip}`),
// Token bucket
c.env.RATE_LIMIT.get(`bucket:${ip}`),
// Multi-tier (free tier)
c.env.RATE_LIMIT.get(`tier:free:${ip}:${minute}`)
]);
return c.json({
ip,
strategies: {
fixedWindow: {
used: fixed ? parseInt(fixed) : 0,
limit: 100,
window: '1 minute'
},
slidingWindow: {
used: sliding ? JSON.parse(sliding).length : 0,
limit: 100,
window: '1 minute rolling'
},
tokenBucket: bucket
? {
tokens: Math.floor(JSON.parse(bucket).tokens),
maxTokens: 100,
refillRate: '~1.67 tokens/sec'
}
: { tokens: 100, maxTokens: 100, refillRate: '~1.67 tokens/sec' },
multiTier: {
used: tier ? parseInt(tier) : 0,
limit: 10,
tier: 'free'
}
}
});
});
// ============================================================================
// Admin: Clear Rate Limits (for testing)
// ============================================================================
app.delete('/api/admin/clear/:ip', async (c) => {
const ip = c.req.param('ip');
const minute = Math.floor(Date.now() / 60000);
// Delete all rate limit keys for IP
await Promise.all([
c.env.RATE_LIMIT.delete(`fixed:${ip}:${minute}`),
c.env.RATE_LIMIT.delete(`fixed:${ip}:${minute - 1}`),
c.env.RATE_LIMIT.delete(`sliding:${ip}`),
c.env.RATE_LIMIT.delete(`bucket:${ip}`),
c.env.RATE_LIMIT.delete(`tier:free:${ip}:${minute}`),
c.env.RATE_LIMIT.delete(`tier:pro:${ip}:${minute}`),
c.env.RATE_LIMIT.delete(`tier:enterprise:${ip}:${minute}`)
]);
return c.json({ success: true, message: `Rate limits cleared for IP: ${ip}` });
});
// ============================================================================
// Root & 404
// ============================================================================
app.get('/', (c) => {
return c.html(`
<h1>Cloudflare Workers KV - Rate Limiting Examples</h1>
<p>Try these endpoints:</p>
<ul>
<li><a href="/api/fixed-window">/api/fixed-window</a> - Fixed window (100/min)</li>
<li><a href="/api/sliding-window">/api/sliding-window</a> - Sliding window (100/min rolling)</li>
<li><a href="/api/token-bucket">/api/token-bucket</a> - Token bucket (allows bursts)</li>
<li><a href="/api/multi-tier">/api/multi-tier</a> - Multi-tier (10/100/1000 per tier)</li>
<li><a href="/api/status">/api/status</a> - Check current status</li>
</ul>
<p>Test by hitting endpoints multiple times to trigger rate limits.</p>
`);
});
app.notFound((c) => {
return c.json({ error: 'Not found' }, 404);
});
export default app;
{
"name": "kv-rate-limiting-example",
"main": "index.ts",
"compatibility_date": "2025-10-11",
"compatibility_flags": ["nodejs_compat"],
// KV namespace for rate limiting
"kv_namespaces": [
{
"binding": "RATE_LIMIT",
"id": "YOUR_PRODUCTION_NAMESPACE_ID",
"preview_id": "YOUR_PREVIEW_NAMESPACE_ID"
}
],
// Development settings
"dev": {
"port": 8787,
"inspector_port": 9229
}
}
/*
* Setup Instructions:
*
* 1. Create KV namespaces:
* bunx wrangler kv namespace create RATE_LIMIT
* bunx wrangler kv namespace create RATE_LIMIT --preview
*
* 2. Update IDs above with output from step 1
*
* 3. Install dependencies:
* bun add hono
* bun add -D @cloudflare/workers-types
*
* 4. Run locally:
* bunx wrangler dev
*
* 5. Deploy:
* bunx wrangler deploy
*
* 6. Test endpoints:
* - https://your-worker.your-subdomain.workers.dev/api/fixed-window
* - https://your-worker.your-subdomain.workers.dev/api/sliding-window
* - https://your-worker.your-subdomain.workers.dev/api/token-bucket
* - https://your-worker.your-subdomain.workers.dev/api/multi-tier
* - https://your-worker.your-subdomain.workers.dev/api/status
*/
/**
* Cloudflare Workers KV - Session Management Example
*
* Production-ready session store using KV with:
* - Secure session creation
* - TTL-based expiration
* - Metadata tracking (IP, user agent, last activity)
* - Session validation
* - Activity tracking
*
* Production-ready Worker with Hono framework
*/
import { Hono } from 'hono';
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
type Bindings = {
SESSIONS: KVNamespace;
};
interface Session {
userId: string;
createdAt: number;
lastActivity: number;
ipAddress: string;
userAgent: string;
data?: Record<string, any>;
}
const app = new Hono<{ Bindings: Bindings }>();
// Session configuration
const SESSION_TTL = 86400; // 24 hours
const SESSION_COOKIE_NAME = 'session_id';
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Generate secure random session ID
*/
function generateSessionId(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
/**
* Get session from KV
*/
async function getSession(
kv: KVNamespace,
sessionId: string
): Promise<Session | null> {
const sessionData = await kv.get(`session:${sessionId}`, 'json');
return sessionData as Session | null;
}
/**
* Save session to KV
*/
async function saveSession(
kv: KVNamespace,
sessionId: string,
session: Session
): Promise<void> {
await kv.put(`session:${sessionId}`, JSON.stringify(session), {
expirationTtl: SESSION_TTL,
metadata: {
userId: session.userId,
ipAddress: session.ipAddress,
lastActivity: session.lastActivity
}
});
}
/**
* Delete session from KV
*/
async function deleteSession(kv: KVNamespace, sessionId: string): Promise<void> {
await kv.delete(`session:${sessionId}`);
}
// ============================================================================
// Session Middleware
// ============================================================================
/**
* Middleware to load and validate session
*/
app.use('*', async (c, next) => {
const sessionId = getCookie(c, SESSION_COOKIE_NAME);
if (sessionId) {
const session = await getSession(c.env.SESSIONS, sessionId);
if (session) {
// Update last activity
session.lastActivity = Date.now();
await saveSession(c.env.SESSIONS, sessionId, session);
// Attach session to context
c.set('session', session);
c.set('sessionId', sessionId);
}
}
await next();
});
// ============================================================================
// Authentication Endpoints
// ============================================================================
/**
* Login endpoint - creates new session
*/
app.post('/auth/login', async (c) => {
const { username, password } = await c.req.json();
// In production, validate credentials against database
// This is a simplified example
if (!username || !password) {
return c.json({ error: 'Missing credentials' }, 400);
}
// Simulate authentication (replace with real auth)
if (password !== 'demo123') {
return c.json({ error: 'Invalid credentials' }, 401);
}
// Create session
const sessionId = generateSessionId();
const session: Session = {
userId: username,
createdAt: Date.now(),
lastActivity: Date.now(),
ipAddress: c.req.header('cf-connecting-ip') || 'unknown',
userAgent: c.req.header('user-agent') || 'unknown',
data: {
loginTimestamp: Date.now(),
role: 'user'
}
};
// Save to KV
await saveSession(c.env.SESSIONS, sessionId, session);
// Set cookie
setCookie(c, SESSION_COOKIE_NAME, sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Lax',
maxAge: SESSION_TTL,
path: '/'
});
return c.json({
success: true,
session: {
userId: session.userId,
expiresAt: Date.now() + SESSION_TTL * 1000
}
});
});
/**
* Logout endpoint - destroys session
*/
app.post('/auth/logout', async (c) => {
const sessionId = c.get('sessionId');
if (sessionId) {
await deleteSession(c.env.SESSIONS, sessionId);
deleteCookie(c, SESSION_COOKIE_NAME);
}
return c.json({ success: true, message: 'Logged out successfully' });
});
/**
* Session status endpoint
*/
app.get('/auth/status', async (c) => {
const session = c.get('session');
const sessionId = c.get('sessionId');
if (!session || !sessionId) {
return c.json({ authenticated: false });
}
return c.json({
authenticated: true,
session: {
userId: session.userId,
createdAt: session.createdAt,
lastActivity: session.lastActivity,
timeRemaining: SESSION_TTL - (Date.now() - session.createdAt) / 1000
}
});
});
// ============================================================================
// Protected Routes
// ============================================================================
/**
* Middleware to require authentication
*/
const requireAuth = async (c: any, next: any) => {
const session = c.get('session');
if (!session) {
return c.json({ error: 'Unauthorized' }, 401);
}
await next();
};
/**
* Protected dashboard endpoint
*/
app.get('/dashboard', requireAuth, async (c) => {
const session = c.get('session');
return c.json({
message: 'Welcome to your dashboard',
user: {
userId: session.userId,
sessionAge: Math.floor((Date.now() - session.createdAt) / 1000),
data: session.data
}
});
});
/**
* Update session data
*/
app.post('/dashboard/update', requireAuth, async (c) => {
const sessionId = c.get('sessionId');
const session = c.get('session');
const updates = await c.req.json();
// Merge updates into session data
session.data = { ...session.data, ...updates };
// Save updated session
await saveSession(c.env.SESSIONS, sessionId, session);
return c.json({ success: true, data: session.data });
});
// ============================================================================
// Admin Endpoints
// ============================================================================
/**
* List all active sessions (for admin)
*/
app.get('/admin/sessions', async (c) => {
const { keys } = await c.env.SESSIONS.list({
prefix: 'session:',
limit: 100
});
const sessions = await Promise.all(
keys.map(async ({ name, metadata }) => {
const sessionId = name.replace('session:', '');
const session = await getSession(c.env.SESSIONS, sessionId);
return {
sessionId,
userId: metadata?.userId,
ipAddress: metadata?.ipAddress,
lastActivity: metadata?.lastActivity,
isActive: session !== null
};
})
);
return c.json({
total: sessions.length,
sessions: sessions.filter(s => s.isActive)
});
});
/**
* Revoke specific session (admin)
*/
app.delete('/admin/sessions/:sessionId', async (c) => {
const sessionId = c.req.param('sessionId');
await deleteSession(c.env.SESSIONS, sessionId);
return c.json({ success: true, message: `Session ${sessionId} revoked` });
});
/**
* Revoke all sessions for user (admin)
*/
app.delete('/admin/users/:userId/sessions', async (c) => {
const userId = c.req.param('userId');
// List all sessions
const { keys } = await c.env.SESSIONS.list({
prefix: 'session:',
limit: 1000
});
// Delete sessions for this user
let deleted = 0;
for (const { name, metadata } of keys) {
if (metadata?.userId === userId) {
const sessionId = name.replace('session:', '');
await deleteSession(c.env.SESSIONS, sessionId);
deleted++;
}
}
return c.json({
success: true,
message: `Revoked ${deleted} sessions for user ${userId}`
});
});
// ============================================================================
// Session Analytics
// ============================================================================
/**
* Track user activity
*/
app.post('/analytics/event', requireAuth, async (c) => {
const session = c.get('session');
const { eventType, eventData } = await c.req.json();
const analyticsKey = `analytics:${session.userId}:${eventType}:${Date.now()}`;
await c.env.SESSIONS.put(
analyticsKey,
JSON.stringify({ eventData, sessionId: c.get('sessionId') }),
{
expirationTtl: 86400 * 30, // Keep for 30 days
metadata: {
userId: session.userId,
eventType,
timestamp: Date.now()
}
}
);
return c.json({ success: true });
});
/**
* Get user activity
*/
app.get('/analytics/user/:userId', async (c) => {
const userId = c.req.param('userId');
const { keys } = await c.env.SESSIONS.list({
prefix: `analytics:${userId}:`,
limit: 100
});
const events = keys.map(({ name, metadata }) => ({
eventType: metadata?.eventType,
timestamp: metadata?.timestamp
}));
return c.json({ userId, events });
});
// ============================================================================
// Root & 404
// ============================================================================
app.get('/', (c) => {
return c.html(`
<h1>Cloudflare Workers KV - Session Management Example</h1>
<p>Try these endpoints:</p>
<h2>Authentication</h2>
<ul>
<li>POST /auth/login - Create session (body: {"username": "test", "password": "demo123"})</li>
<li>POST /auth/logout - Destroy session</li>
<li>GET /auth/status - Check session status</li>
</ul>
<h2>Protected Routes</h2>
<ul>
<li>GET /dashboard - Access protected dashboard</li>
<li>POST /dashboard/update - Update session data</li>
</ul>
<h2>Admin</h2>
<ul>
<li>GET /admin/sessions - List all active sessions</li>
<li>DELETE /admin/sessions/:sessionId - Revoke specific session</li>
<li>DELETE /admin/users/:userId/sessions - Revoke all user sessions</li>
</ul>
<h2>Analytics</h2>
<ul>
<li>POST /analytics/event - Track user event</li>
<li>GET /analytics/user/:userId - Get user activity</li>
</ul>
`);
});
app.notFound((c) => {
return c.json({ error: 'Not found' }, 404);
});
export default app;
{
"name": "kv-session-management-example",
"main": "index.ts",
"compatibility_date": "2025-10-11",
"compatibility_flags": ["nodejs_compat"],
// KV namespace for sessions
"kv_namespaces": [
{
"binding": "SESSIONS",
"id": "YOUR_PRODUCTION_NAMESPACE_ID",
"preview_id": "YOUR_PREVIEW_NAMESPACE_ID"
}
],
// Development settings
"dev": {
"port": 8787,
"inspector_port": 9229
}
}
/*
* Setup Instructions:
*
* 1. Create KV namespaces:
* bunx wrangler kv namespace create SESSIONS
* bunx wrangler kv namespace create SESSIONS --preview
*
* 2. Update IDs above with output from step 1
*
* 3. Install dependencies:
* bun add hono
* bun add -D @cloudflare/workers-types
*
* 4. Run locally:
* bunx wrangler dev
*
* 5. Deploy:
* bunx wrangler deploy
*
* 6. Test with curl:
* # Login
* curl -X POST https://your-worker.workers.dev/auth/login \
* -H "Content-Type: application/json" \
* -d '{"username": "test", "password": "demo123"}' \
* -c cookies.txt
*
* # Check status (uses cookie from login)
* curl https://your-worker.workers.dev/auth/status \
* -b cookies.txt
*
* # Access protected dashboard
* curl https://your-worker.workers.dev/dashboard \
* -b cookies.txt
*
* # Logout
* curl -X POST https://your-worker.workers.dev/auth/logout \
* -b cookies.txt
*/
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 - Limits & Quotas
Comprehensive reference for KV limits, quotas, and pricing.
---
Storage Limits
Key Size
- Maximum: 512 bytes (UTF-8 encoded)
- Recommendation: Keep keys short (50-100 bytes)
- Example:
user:123:preferences(22 bytes)
Value Size
- Maximum: 25 MB per value
- Recommendation: Store large files in R2, use KV for metadata
- Calculation:
const sizeInBytes = new Blob([value]).size;
if (sizeInBytes > 25 * 1024 * 1024) {
throw new Error('Value exceeds 25MB limit');
}Metadata Size
- Maximum: 1024 bytes (1 KB) per key
- Use case: Store small structured data alongside values
- Example:
await env.KV.put('key', value, {
metadata: {
created: Date.now(),
author: 'user123',
version: '1.0'
} // Must be <1KB when JSON stringified
});Namespace Limits
- Maximum keys: Unlimited
- Maximum namespaces: Unlimited
- Recommendation: Organize with prefixes vs creating many namespaces
---
Operation Rate Limits
Write Operations (put/delete)
- Limit: 1000 operations/second per key
- Scope: Per key, not per namespace
- Example:
// ❌ Will hit rate limit (>1000 writes/sec to same key)
for (let i = 0; i < 2000; i++) {
await env.KV.put('counter', String(i));
}
// ✅ Won't hit rate limit (different keys)
for (let i = 0; i < 2000; i++) {
await env.KV.put(`counter:${i}`, String(i));
}Rate Limit Response:
- HTTP Status:
429 Too Many Requests - Error: "Rate limit exceeded"
- Solution: Implement exponential backoff or distribute across keys
Read Operations (get)
- Limit: Unlimited
- cacheTtl: Enables edge caching (highly recommended)
- Performance: ~1-5ms with cacheTtl vs ~50-200ms without
List Operations
- Limit: 100 operations/second per namespace
- Maximum keys per list(): 1000
- Pagination: Required for >1000 keys
- Example:
async function listAll(kv, prefix = '') {
let keys = [];
let cursor;
do {
const result = await kv.list({ prefix, cursor, limit: 1000 });
keys.push(...result.keys);
cursor = result.cursor;
} while (cursor);
return keys;
}---
Worker-Specific Limits
Operations Per Invocation
- Maximum: 1000 KV operations per Worker invocation
- Includes: All get/put/delete/list calls combined
- Workaround: Use bulk operations where possible
- Example:
// ❌ 100 operations
for (let i = 0; i < 100; i++) {
await env.KV.get(`key${i}`);
}
// ✅ 1 operation (if supported by API)
const values = await env.KV.get(['key0', 'key1', ...]);CPU Time
- Workers have 50ms CPU time limit (free) or 30s (paid)
- Large KV operations count toward this
- Use
ctx.waitUntil()for non-critical operations
---
Pricing
Free Tier
- Read operations: 100,000/day
- Write operations: 1,000/day
- Delete operations: 1,000/day
- Storage: 1 GB
- List operations: Included in write quota
Paid Tier (Workers Paid Plan)
- Read operations: $0.50 per million reads
- Write operations: $5.00 per million writes
- Delete operations: $5.00 per million deletes
- Storage: $0.50 per GB-month
- List operations: Counted as read operations
Cost Examples
Example 1: Configuration Storage
- 1000 config updates/day (writes)
- 1,000,000 config reads/day (reads)
- 10 MB storage
Monthly Cost:
- Writes: 30,000 × $5/million = $0.15
- Reads: 30M × $0.50/million = $15.00
- Storage: 0.01 GB × $0.50 = $0.005
- Total: ~$15.15/month
Example 2: Session Management
- 100,000 sessions/day (writes)
- 500,000 session reads/day (reads)
- 1 GB storage (TTL=24h keeps it bounded)
Monthly Cost:
- Writes: 3M × $5/million = $15.00
- Reads: 15M × $0.50/million = $7.50
- Storage: 1 GB × $0.50 = $0.50
- Total: ~$23.00/month
Cost Optimization Tips
1. Use TTL to reduce storage costs
// Auto-expire temporary data
await env.KV.put('session', data, {
expirationTtl: 86400 // 24 hours
});2. Use cacheTtl to reduce read operations
// Cache at edge = fewer KV reads
const config = await env.KV.get('config', {
cacheTtl: 3600 // 1 hour
});3. Coalesce small values
// ❌ 5 write operations
await env.KV.put('user:name', name);
await env.KV.put('user:email', email);
await env.KV.put('user:age', age);
await env.KV.put('user:city', city);
await env.KV.put('user:country', country);
// ✅ 1 write operation
await env.KV.put('user', JSON.stringify({
name, email, age, city, country
}));4. Use metadata for small data
// Metadata is included in get() at no extra cost
await env.KV.put('key', mainValue, {
metadata: { count: 123, updated: Date.now() }
});
const { value, metadata } = await env.KV.getWithMetadata('key');
// One operation, two pieces of data5. Use waitUntil() for non-critical writes
// Don't wait for analytics writes
ctx.waitUntil(
env.KV.put('analytics', data)
);
// Response returns immediately, write happens in background---
Quota Monitoring
Check Current Usage
Via Cloudflare Dashboard: 1. Log in to Cloudflare Dashboard 2. Navigate to Workers & Pages 3. Select your Worker 4. View Metrics tab 5. Check KV operations graph
Via API:
curl -X GET "https://api.cloudflare.com/client/v4/accounts/{account_id}/storage/kv/namespaces/{namespace_id}" \
-H "Authorization: Bearer {api_token}"Set Up Alerts
Create alerts for approaching limits: 1. Dashboard → Notifications 2. Create KV usage alert 3. Set threshold (e.g., 80% of daily quota) 4. Configure notification method (email/webhook)
---
Limit Workarounds
For High-Frequency Writes (>1000/sec to same key)
Problem: Need to update counter >1000 times/second
Solutions: 1. Use Durable Objects (designed for high-frequency state) 2. Distribute across keys
const shardKey = `counter:shard${Math.floor(Math.random() * 10)}`;
await env.KV.put(shardKey, newValue);3. Batch updates
// Accumulate in memory, write periodically
let batch = [];
setInterval(() => {
env.KV.put('batch', JSON.stringify(batch));
batch = [];
}, 1000);For Large Values (>25 MB)
Problem: Need to store 100MB file
Solution: Use R2 for objects, KV for metadata
// Upload large file to R2
await env.R2.put('file.pdf', fileData);
// Store metadata in KV
await env.KV.put('file:metadata', JSON.stringify({
r2Key: 'file.pdf',
size: 100_000_000,
type: 'application/pdf'
}));For Strong Consistency
Problem: Need immediate global consistency
Solution: Use D1 or Durable Objects
// KV: Eventually consistent, optimized for reads
await env.KV.put('config', value); // May take 60s to propagate
// D1: Strongly consistent, optimized for transactions
await env.DB.prepare('UPDATE config SET value = ?').bind(value).run();---
Comparison with Alternatives
| Feature | KV | D1 | R2 | Durable Objects |
|---|---|---|---|---|
| Consistency | Eventual | Strong | Strong | Strong |
| Max Value Size | 25 MB | Row-based | 5 TB | Memory-limited |
| Write Limit | 1000/sec/key | DB limits | Unlimited | Unlimited |
| Best For | Config, cache | Relational data | Large files | State, counters |
| Read Performance | Excellent (with cacheTtl) | Good | Good | Excellent |
| Global Distribution | Yes | Regional | Yes | Global |
---
Best Practices for Staying Within Limits
1. Plan for quotas - Estimate usage before building 2. Use TTL aggressively - Reduce storage costs 3. Leverage cacheTtl - Reduce read operations 4. Monitor usage - Set up alerts 5. Design for eventual consistency - Don't fight KV's nature 6. Use right tool for job - KV for reads, D1 for transactions, R2 for files 7. Test with production data - Validate assumptions
---
Last Updated: 2025-12-27 Official Limits: https://developers.cloudflare.com/kv/platform/limits/
Cloudflare Workers KV - Migration Guide
Complete guide for migrating to Workers KV from various storage solutions.
---
Table of Contents
1. From localStorage/sessionStorage 2. From Redis 3. From D1 Database 4. From R2 Object Storage 5. From Other KV Solutions 6. Testing Migration 7. Rollback Procedures 8. Production Cutover
---
From localStorage/sessionStorage
Why Migrate?
Current limitations:
- Client-side only (not accessible from server)
- 5-10MB storage limit per domain
- No cross-device synchronization
- Browser-dependent (can be cleared)
- No server-side validation
KV advantages:
- Global edge storage
- Accessible from Workers (server-side)
- 25MB per value (unlimited total)
- Cross-device sync via Workers
- TTL-based expiration
- Server-side validation and security
Migration Strategy
1. Identify Data to Migrate
// Client-side analysis
const localStorageKeys = Object.keys(localStorage);
const sessionStorageKeys = Object.keys(sessionStorage);
console.log('localStorage items:', localStorageKeys.length);
console.log('sessionStorage items:', sessionStorageKeys.length);
// Analyze size
let totalSize = 0;
localStorageKeys.forEach(key => {
totalSize += localStorage.getItem(key)?.length || 0;
});
console.log('Total localStorage size:', totalSize, 'bytes');2. Create Migration Worker
import { Hono } from 'hono';
type Bindings = {
USER_DATA: KVNamespace;
};
const app = new Hono<{ Bindings: Bindings }>();
// Migration endpoint
app.post('/migrate', async (c) => {
const { userId, data } = await c.req.json();
// Validate data
if (!userId || !data) {
return c.json({ error: 'Missing userId or data' }, 400);
}
// Migrate each localStorage item to KV
for (const [key, value] of Object.entries(data)) {
await c.env.USER_DATA.put(
`user:${userId}:${key}`,
JSON.stringify(value),
{
expirationTtl: 86400 * 30, // 30 days
metadata: {
migratedAt: Date.now(),
source: 'localStorage'
}
}
);
}
return c.json({
success: true,
itemsMigrated: Object.keys(data).length
});
});
// Read endpoint (replaces localStorage.getItem)
app.get('/data/:userId/:key', async (c) => {
const { userId, key } = c.req.param();
const value = await c.env.USER_DATA.get(`user:${userId}:${key}`, 'json');
if (!value) {
return c.json({ error: 'Not found' }, 404);
}
return c.json({ value });
});
export default app;3. Client-Side Migration Script
// Run once per user to migrate data
async function migrateToKV(userId: string) {
// Collect all localStorage data
const data: Record<string, any> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
try {
data[key] = JSON.parse(localStorage.getItem(key) || '');
} catch {
data[key] = localStorage.getItem(key);
}
}
}
// Send to migration endpoint
const response = await fetch('/migrate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, data })
});
if (response.ok) {
console.log('Migration successful!');
// Optionally clear localStorage after successful migration
// localStorage.clear();
}
}4. Update Application Code
// Before (localStorage)
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
// After (KV via Worker API)
await fetch('/data/user123/theme', {
method: 'PUT',
body: JSON.stringify({ value: 'dark' })
});
const response = await fetch('/data/user123/theme');
const { value: theme } = await response.json();---
From Redis
Why Migrate?
Redis strengths:
- In-memory speed
- Complex data structures (sets, sorted sets, etc.)
- Pub/Sub
- Atomic operations
- Strong consistency
When to use KV instead:
- Read-heavy workloads (KV has cacheTtl)
- Global edge distribution needed
- Don't need atomic operations
- Don't need complex data structures
- Cost optimization (Redis hosting can be expensive)
When NOT to migrate:
- Need atomic operations (INCR, etc.)
- Need complex data structures (sorted sets, etc.)
- Need strong consistency
- Need Pub/Sub
- Write-heavy workloads
Migration Strategy
1. Analyze Redis Usage
# Connect to Redis
redis-cli
# Check data types
KEYS *
TYPE user:123:preferences
TYPE counter:*
# Identify patterns
SCAN 0 MATCH user:* COUNT 10002. Compatible Patterns
// ✅ Simple key-value (easy migration)
// Redis
await redis.set('config', JSON.stringify(config));
const config = JSON.parse(await redis.get('config'));
// KV
await env.KV.put('config', JSON.stringify(config));
const config = await env.KV.get('config', 'json');
// ✅ TTL expiration (easy migration)
// Redis
await redis.setex('session:123', 3600, sessionData);
// KV
await env.KV.put('session:123', sessionData, {
expirationTtl: 3600
});
// ✅ Key prefix patterns (easy migration)
// Redis
await redis.keys('user:*');
// KV
await env.KV.list({ prefix: 'user:' });3. Incompatible Patterns (Need Workarounds)
// ❌ Atomic increment (use Durable Objects instead)
// Redis
await redis.incr('counter');
// KV workaround (not atomic, eventual consistency issues)
const count = parseInt(await env.KV.get('counter') || '0');
await env.KV.put('counter', String(count + 1));
// ✅ Better: Use Durable Objects for counters
export class Counter {
state: DurableObjectState;
count = 0;
async increment() {
this.count++;
await this.state.storage.put('count', this.count);
return this.count;
}
}
// ❌ Sorted sets (use D1 instead)
// Redis
await redis.zadd('leaderboard', score, userId);
// D1
await env.DB.prepare(`
INSERT INTO leaderboard (user_id, score)
VALUES (?, ?)
ON CONFLICT (user_id) DO UPDATE SET score = ?
`).bind(userId, score, score).run();
// ❌ Pub/Sub (use Queues or Durable Objects)
// Redis
redis.subscribe('notifications');
// Cloudflare Queues
await env.QUEUE.send({ type: 'notification', data });4. Migration Worker
import { Hono } from 'hono';
import { Redis } from '@upstash/redis';
type Bindings = {
KV: KVNamespace;
REDIS_URL: string;
REDIS_TOKEN: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.post('/migrate-redis', async (c) => {
const redis = new Redis({
url: c.env.REDIS_URL,
token: c.env.REDIS_TOKEN
});
let cursor = '0';
let migrated = 0;
do {
// Scan Redis keys
const [newCursor, keys] = await redis.scan(cursor, {
match: '*',
count: 100
});
cursor = newCursor;
// Migrate each key
for (const key of keys) {
const value = await redis.get(key);
const ttl = await redis.ttl(key);
if (value) {
const options: any = {};
if (ttl > 0) {
options.expirationTtl = ttl;
}
await c.env.KV.put(key, value, options);
migrated++;
}
}
} while (cursor !== '0');
return c.json({ migrated });
});
export default app;---
From D1 Database
Why Migrate?
D1 strengths:
- SQL queries
- Relational data
- Transactions
- Strong consistency
- Joins
When to use KV instead:
- Simple key-value lookups
- No relational requirements
- Read-heavy workloads
- Don't need transactions
- Global edge caching needed
When NOT to migrate:
- Need SQL queries
- Need relationships/joins
- Need transactions
- Need strong consistency
- Need complex filtering
Migration Strategy
1. Identify Non-Relational Data
-- Good candidates for KV
SELECT * FROM config WHERE key = ?; -- Simple key-value
SELECT * FROM user_preferences WHERE user_id = ?; -- User-specific data
SELECT * FROM cache WHERE key = ?; -- Already cache-like
-- Bad candidates (keep in D1)
SELECT u.*, p.* FROM users u JOIN posts p ON u.id = p.user_id; -- Relations
SELECT * FROM orders WHERE status = 'pending' AND created_at > ?; -- Complex queries2. Extract Data from D1
// Export config table to KV
const configs = await env.DB.prepare('SELECT key, value FROM config').all();
for (const row of configs.results) {
await env.KV.put(
`config:${row.key}`,
row.value,
{
metadata: { migratedFrom: 'd1', table: 'config' }
}
);
}3. Dual-Write Pattern (Safe Migration)
// During migration, write to both D1 and KV
async function setConfig(key: string, value: string) {
// Write to D1 (source of truth during migration)
await env.DB.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)')
.bind(key, value)
.run();
// Also write to KV
await env.KV.put(`config:${key}`, value);
}
// Read from KV, fallback to D1
async function getConfig(key: string) {
// Try KV first (faster)
let value = await env.KV.get(`config:${key}`);
if (!value) {
// Fallback to D1
const result = await env.DB.prepare('SELECT value FROM config WHERE key = ?')
.bind(key)
.first();
if (result) {
value = result.value as string;
// Backfill KV
await env.KV.put(`config:${key}`, value);
}
}
return value;
}4. Validate and Cutover
// Validation script
async function validateMigration() {
const d1Configs = await env.DB.prepare('SELECT key, value FROM config').all();
let mismatches = 0;
for (const row of d1Configs.results) {
const kvValue = await env.KV.get(`config:${row.key}`);
if (kvValue !== row.value) {
console.error(`Mismatch for ${row.key}: D1=${row.value}, KV=${kvValue}`);
mismatches++;
}
}
return { total: d1Configs.results.length, mismatches };
}---
From R2 Object Storage
Why Migrate?
R2 strengths:
- Large files (up to 5 TB)
- S3-compatible API
- Multipart uploads
- No egress fees
When to use KV instead:
- Small values (<25 MB)
- Metadata-heavy operations
- Need TTL expiration
- Need list operations
- Read-heavy with edge caching
When NOT to migrate:
- Files >25 MB
- Need S3 compatibility
- Need multipart uploads
- Binary files (images, videos, PDFs)
Migration Strategy
Use KV for metadata, R2 for files:
// Store large file in R2
await env.R2.put('uploads/file.pdf', fileData);
// Store metadata in KV
await env.KV.put('file:metadata:file.pdf', JSON.stringify({
r2Key: 'uploads/file.pdf',
size: fileData.size,
type: 'application/pdf',
uploadedAt: Date.now()
}), {
metadata: { source: 'r2-migration' }
});
// Retrieve
const metadata = await env.KV.get('file:metadata:file.pdf', 'json');
const file = await env.R2.get(metadata.r2Key);---
Testing Migration
Pre-Migration Checklist
- [ ] Identify all data to migrate
- [ ] Map Redis/D1 patterns to KV equivalents
- [ ] Create migration scripts
- [ ] Set up dual-write if needed
- [ ] Create validation scripts
- [ ] Test in preview environment
Validation Script
async function validateMigration(env: Bindings) {
const errors: string[] = [];
// Test 1: Verify data integrity
const sampleKeys = ['config:theme', 'user:123:preferences'];
for (const key of sampleKeys) {
const value = await env.KV.get(key);
if (!value) {
errors.push(`Missing key: ${key}`);
}
}
// Test 2: Verify TTL
const ttlKey = 'session:test';
await env.KV.put(ttlKey, 'test', { expirationTtl: 60 });
const ttlValue = await env.KV.get(ttlKey);
if (!ttlValue) {
errors.push('TTL test failed');
}
// Test 3: Verify metadata
const { value, metadata } = await env.KV.getWithMetadata('config:theme');
if (!metadata?.migratedFrom) {
errors.push('Metadata missing on migrated keys');
}
return { success: errors.length === 0, errors };
}---
Rollback Procedures
Immediate Rollback
If issues occur during migration:
// Stop dual-write, revert to source
async function rollback() {
// 1. Switch reads back to source (Redis/D1)
// 2. Stop writing to KV
// 3. Log rollback event
console.log('Rolling back to source system');
// Example: Revert to D1
async function getConfig(key: string) {
// Read from D1 only
const result = await env.DB.prepare('SELECT value FROM config WHERE key = ?')
.bind(key)
.first();
return result?.value;
}
}Data Preservation
// Before deleting source data, verify KV
async function safeDelete() {
const d1Count = await env.DB.prepare('SELECT COUNT(*) as count FROM config').first();
const kvCount = (await env.KV.list()).keys.length;
if (kvCount < d1Count.count) {
throw new Error('KV has fewer records than D1. Aborting deletion.');
}
console.log('Safe to delete source data');
}---
Production Cutover
Cutover Checklist
Pre-Cutover (1 week before):
- [ ] Complete all data migration
- [ ] Validate data integrity
- [ ] Test application with KV
- [ ] Set up monitoring
- [ ] Document rollback procedure
- [ ] Schedule maintenance window
During Cutover:
- [ ] Enable dual-write mode
- [ ] Monitor error rates
- [ ] Verify read performance
- [ ] Check data consistency
- [ ] Monitor KV metrics in dashboard
Post-Cutover (1 week after):
- [ ] Disable dual-write (KV only)
- [ ] Monitor for issues
- [ ] Verify cost savings
- [ ] Archive source data
- [ ] Update documentation
Monitoring During Cutover
// Add logging to track migration
async function getConfigWithLogging(key: string) {
const start = Date.now();
// Try KV
const kvValue = await env.KV.get(`config:${key}`);
const kvTime = Date.now() - start;
// Log performance
console.log({
key,
source: kvValue ? 'kv' : 'fallback',
latency: kvTime,
timestamp: Date.now()
});
return kvValue;
}Success Metrics
Monitor these metrics:
- Latency: KV reads should be <50ms (with cacheTtl: <5ms)
- Error rate: Should be <0.1%
- Cache hit rate: >90% for frequently accessed keys
- Cost: Compare KV operations cost vs source system
- Data consistency: 100% match between source and KV
---
Common Migration Patterns
Pattern 1: User Preferences
// Before (D1)
const prefs = await env.DB.prepare(
'SELECT * FROM user_preferences WHERE user_id = ?'
).bind(userId).first();
// After (KV)
const prefs = await env.KV.get(`user:${userId}:preferences`, 'json');Pattern 2: Configuration
// Before (Redis)
const config = await redis.get('app:config');
// After (KV with cacheTtl)
const config = await env.KV.get('app:config', {
type: 'json',
cacheTtl: 300 // Cache for 5 minutes
});Pattern 3: Session Management
// Before (Redis with TTL)
await redis.setex(`session:${sessionId}`, 3600, sessionData);
// After (KV with expirationTtl)
await env.KV.put(`session:${sessionId}`, sessionData, {
expirationTtl: 3600
});---
Last Updated: 2025-12-27 Related: troubleshooting.md, best-practices.md, limits-quotas.md
Cloudflare Workers KV Complete Setup
Quick setup for global key-value storage on Cloudflare edge.
---
Step 1: Create KV Namespace
# Production namespace
npx wrangler kv namespace create MY_NAMESPACE
# Preview namespace (for dev)
npx wrangler kv namespace create MY_NAMESPACE --previewSave the id and preview_id!
---
Step 2: Configure Binding
Add to wrangler.jsonc:
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"kv_namespaces": [
{
"binding": "MY_NAMESPACE", // env.MY_NAMESPACE
"id": "<PRODUCTION_ID>",
"preview_id": "<PREVIEW_ID>"
}
]
}---
Step 3: Use in Worker
export default {
async fetch(request, env, ctx) {
// Write
await env.MY_NAMESPACE.put('key', 'value');
// Read
const value = await env.MY_NAMESPACE.get('key');
// Delete
await env.MY_NAMESPACE.delete('key');
return new Response(value);
}
};---
Common Patterns
With TTL
await env.MY_NAMESPACE.put('key', 'value', {
expirationTtl: 3600 // 1 hour
});With Metadata
await env.MY_NAMESPACE.put('user:123', JSON.stringify({ name: 'Alice' }), {
metadata: { role: 'admin', created: Date.now() }
});
const { value, metadata } = await env.MY_NAMESPACE.getWithMetadata('user:123');List Keys
const { keys } = await env.MY_NAMESPACE.list({
prefix: 'user:',
limit: 100
});---
Official Documentation
- KV Overview: https://developers.cloudflare.com/kv/
- KV API: https://developers.cloudflare.com/kv/api/
#!/bin/bash
# Cloudflare Workers KV - Version Checker
# Verifies KV API endpoints and package versions
echo "Cloudflare Workers KV - Version Checker"
echo "========================================"
echo ""
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Check if wrangler is installed
if command -v wrangler &> /dev/null; then
WRANGLER_VERSION=$(wrangler --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo -e "${GREEN}✓${NC} Wrangler installed: v$WRANGLER_VERSION"
else
echo -e "${YELLOW}⚠${NC} Wrangler not installed (optional but recommended)"
echo " Install: npm install -g wrangler"
fi
echo ""
echo "Checking KV API availability..."
echo ""
# Check KV API endpoint (no auth required for availability check)
echo -n "Checking KV API endpoint... "
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
"https://api.cloudflare.com/client/v4/accounts" 2>/dev/null)
if [ "$RESPONSE" = "403" ] || [ "$RESPONSE" = "401" ]; then
echo -e "${GREEN}✓ Available (authentication required)${NC}"
elif [ "$RESPONSE" = "200" ]; then
echo -e "${GREEN}✓ Available${NC}"
else
echo -e "${YELLOW}⚠ Unable to verify (HTTP $RESPONSE)${NC}"
fi
echo ""
echo "Package Recommendations:"
echo "========================"
echo ""
# Check for @cloudflare/workers-types
if [ -f "package.json" ]; then
if grep -q "@cloudflare/workers-types" package.json; then
VERSION=$(grep "@cloudflare/workers-types" package.json | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo -e "${GREEN}✓${NC} @cloudflare/workers-types: v$VERSION (installed)"
else
echo -e "${YELLOW}⚠${NC} @cloudflare/workers-types: Not found in package.json"
echo " Install: npm install -D @cloudflare/workers-types@latest"
fi
else
echo -e "${YELLOW}⚠${NC} No package.json found"
echo " For TypeScript support, install: @cloudflare/workers-types@latest"
fi
echo ""
echo "KV Documentation:"
echo "================="
echo "• API Docs: https://developers.cloudflare.com/kv/api/"
echo "• Best Practices: https://developers.cloudflare.com/kv/best-practices/"
echo "• Wrangler Docs: https://developers.cloudflare.com/workers/wrangler/"
echo ""
echo "KV API Version: v4 (current)"
echo "Last Verified: 2025-12-27"
echo ""
echo -e "${GREEN}✓ KV API endpoints available${NC}"
{
"$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
}