kv
Workers KV
Fast, eventually-consistent key-value storage distributed globally across Cloudflare's edge network.
FIRST: Create KV Namespace
# Create namespace
wrangler kv namespace create MY_KV
# Create preview namespace for dev
wrangler kv namespace create MY_KV --preview
# Add to wrangler.jsonc (use IDs from commands above)
When to Use
| Use Case | Why KV |
|---|---|
| Session storage | Fast token/session lookups at the edge |
| Caching | Cache API responses, rendered HTML, or computed results |
| Configuration data | Feature flags, app settings, rate limit counters |
| User profiles | Store user preferences, settings, metadata |
| A/B testing | Store experiment assignments and feature toggles |
| Rate limiting | Track request counts per user/IP with TTL |
Don't use KV for:
- Strongly consistent data (use Durable Objects)
- Relational queries (use D1)
- Large files >25MB (use R2)
- Transactional operations
Quick Reference
| Operation | API | Example |
|---|---|---|
| Read value | get(key) |
await env.KV.get("user:123") |
| Read with metadata | getWithMetadata(key) |
await env.KV.getWithMetadata("key", "json") |
| Write value | put(key, value, options) |
await env.KV.put("key", "value", { expirationTtl: 3600 }) |
| Delete value | delete(key) |
await env.KV.delete("key") |
| List keys | list(options) |
await env.KV.list({ prefix: "user:" }) |
Value Types
// String (default)
await env.KV.get("key")
// JSON (auto-parse)
await env.KV.get("user:123", "json")
// Binary
await env.KV.get("image", "arrayBuffer")
// Stream
await env.KV.get("large-file", "stream")
wrangler.jsonc Configuration
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-01-01",
"kv_namespaces": [
{
"binding": "AUTH_TOKENS",
"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"preview_id": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
},
{
"binding": "CACHE",
"id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
}
]
}
After adding bindings, generate types:
wrangler types
Session Authentication Pattern
Complete session management using KV for token storage with Hono router:
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
interface Env {
AUTH_TOKENS: KVNamespace;
}
const app = new Hono<{ Bindings: Env }>()
// Add CORS middleware
app.use('*', cors())
app.get('/', async (c) => {
try {
// Get token from header or cookie
const token = c.req.header('Authorization')?.slice(7) ||
c.req.header('Cookie')?.match(/auth_token=([^;]+)/)?.[1];
if (!token) {
return c.json({
authenticated: false,
message: 'No authentication token provided'
}, 403)
}
// Check token in KV
const userData = await c.env.AUTH_TOKENS.get(token)
if (!userData) {
return c.json({
authenticated: false,
message: 'Invalid or expired token'
}, 403)
}
return c.json({
authenticated: true,
message: 'Authentication successful',
data: JSON.parse(userData)
})
} catch (error) {
console.error('Authentication error:', error)
return c.json({
authenticated: false,
message: 'Internal server error'
}, 500)
}
})
export default app
Store a session token:
// Create token (e.g., on login)
const token = crypto.randomUUID()
const userData = { userId: 123, email: "user@example.com" }
// Store with 24-hour expiration
await env.AUTH_TOKENS.put(
token,
JSON.stringify(userData),
{ expirationTtl: 86400 }
)
Key Design Patterns
TTL (Time-to-Live)
// Expire after 1 hour
await env.KV.put("cache:api-response", data, {
expirationTtl: 3600
})
// Expire at specific time
await env.KV.put("promo:summer", data, {
expiration: Math.floor(Date.now() / 1000) + 86400
})
Key Naming Conventions
// Use prefixes for organization and listing
"user:123" // User data
"session:abc123" // Session tokens
"cache:api:/users" // API cache
"config:feature:new" // Feature flags
"rate:ip:1.2.3.4" // Rate limiting
// List all user keys
const { keys } = await env.KV.list({ prefix: "user:" })
Metadata
// Store metadata with value
await env.KV.put("user:123", userData, {
metadata: {
created: Date.now(),
version: "v1",
tags: ["premium", "verified"]
}
})
// Read with metadata
const { value, metadata } = await env.KV.getWithMetadata("user:123", "json")
console.log(metadata.tags) // ["premium", "verified"]
Caching Pattern
async function getCachedData(env: Env, key: string) {
// Try cache first
const cached = await env.CACHE.get(key, "json")
if (cached) return cached
// Fetch from origin
const data = await fetchFromAPI()
// Cache for 5 minutes
await env.CACHE.put(key, JSON.stringify(data), {
expirationTtl: 300
})
return data
}
Rate Limiting
async function checkRateLimit(env: Env, ip: string): Promise<boolean> {
const key = `rate:${ip}`
const count = await env.KV.get(key)
if (!count) {
// First request
await env.KV.put(key, "1", { expirationTtl: 60 })
return true
}
const current = parseInt(count)
if (current >= 100) {
return false // Rate limit exceeded
}
// Increment counter
await env.KV.put(key, String(current + 1), { expirationTtl: 60 })
return true
}
Edge Cases
Handling 404s (Key Not Found)
const value = await env.KV.get("key")
if (value === null) {
// Key doesn't exist or has expired
console.log("Key not found")
}
// For JSON, check before parsing
const userData = await env.KV.get("user:123")
if (userData) {
const user = JSON.parse(userData)
}
// Or use "json" type (returns null if not found)
const user = await env.KV.get("user:123", "json")
Eventual Consistency
// Write may take seconds to propagate globally
await env.KV.put("counter", "1")
// Immediate read might return old value or null
const value = await env.KV.get("counter") // Could be null or "1"
// For strong consistency, use Durable Objects instead
Size Limits
// Keys: max 512 bytes
// Values: max 25 MB
// Metadata: max 1024 bytes
// For larger data, use R2:
if (data.length > 25 * 1024 * 1024) {
await env.BUCKET.put("large-file", data)
} else {
await env.KV.put("small-file", data)
}
List Pagination
// List returns max 1000 keys per call
let cursor: string | undefined
const allKeys: string[] = []
do {
const result = await env.KV.list({
prefix: "user:",
cursor
})
allKeys.push(...result.keys.map(k => k.name))
cursor = result.list_complete ? undefined : result.cursor
} while (cursor)
Detailed References
- references/patterns.md - Advanced patterns: cache invalidation, distributed locks, A/B testing
- references/limits.md - Size limits, rate limits, consistency model
- references/testing.md - Vitest integration, mocking KV, test isolation
Best Practices
- Use TTL for temporary data: Always set expiration for sessions, caches, rate limits
- Prefix your keys: Makes listing and management easier (
user:,session:,cache:) - Don't rely on immediate consistency: KV is eventually consistent (usually <60s)
- Handle null values: Keys not found or expired return
null - Use metadata for filtering: Store timestamps, versions, tags without reading values
- Batch operations: Use
list()with prefix instead of individual gets - Cache at the edge: KV is fastest when accessed from same region as user
- Monitor costs: Writes are more expensive than reads, optimize write patterns
- Use Durable Objects for coordination: Don't use KV for locks or transactions
- Generate types: Run
wrangler typesafter config changes for type safety
More from null-shot/cloudflare-skills
cloudflare-opennext
Deploy Next.js to Cloudflare Workers with full App Router, Pages Router, ISR, and SSG support. Load when creating Next.js projects for Workers, migrating from Vercel/next-on-pages, configuring caching (R2/KV/D1), accessing Cloudflare bindings via getCloudflareContext, or fixing bundle size issues.
195browser-rendering
Headless Chrome automation for web scraping, screenshots, PDFs, and testing at the edge. Load when capturing page screenshots, generating PDFs, scraping dynamic content, extracting structured data, or automating browser interactions. Supports REST API, Puppeteer, Playwright, and Stagehand.
17r2-storage
S3-compatible object storage for files, images, and large data. Load when handling file uploads, storing images/videos/documents, generating presigned URLs, using multipart uploads for large files, migrating from S3, or serving static assets from buckets.
16workers
Core Workers fundamentals including handlers, configuration, and Service Bindings. Load when creating new Workers, configuring wrangler.jsonc, implementing fetch/scheduled/queue handlers, using Service Bindings for RPC, generating types with wrangler types, or building microservices.
10queues
Asynchronous message queues for reliable background processing. Load when offloading background tasks, batch processing messages, implementing retry logic with dead letter queues, rate limiting upstream APIs, or decoupling producers from consumers.
10wrangler
Cloudflare Workers CLI for deploying, developing, and managing Workers, KV, R2, D1, Vectorize, Hyperdrive, Workers AI, Containers, Queues, Workflows, Pipelines, and Secrets Store. Load before running wrangler commands to ensure correct syntax and best practices.
7