TL;DR — Quick Summary
Cloudflare Workers KV provides globally distributed key-value storage for edge applications. Learn setup, CRUD operations, caching patterns and limits.
Cloudflare Workers KV is a globally distributed key-value data store that runs on Cloudflare’s edge network across 300+ cities worldwide. It enables you to store and retrieve data with low-latency reads at the edge, making it ideal for configuration data, feature flags, URL shorteners, A/B testing, and serving personalized content. This guide covers everything from setup to production patterns.
Prerequisites
- A Cloudflare account (free tier works)
- Node.js 18+ installed
- Wrangler CLI (
npm install -g wrangler) - Basic familiarity with JavaScript/TypeScript
- A Cloudflare Workers project (or willingness to create one)
Understanding Workers KV Architecture
Workers KV is an eventually consistent data store. Writes propagate globally within approximately 60 seconds, but reads from the same data center where the write occurred are immediately consistent. This architecture prioritizes read performance over write consistency.
Write → Central Store → Propagate to 300+ Edge Nodes (≤60s)
Read → Nearest Edge Node → Cache Hit (fast) or Central Fetch
When to use KV:
- Configuration and feature flags
- Session/authentication data (eventual consistency acceptable)
- URL shorteners and redirects
- Internationalization strings
- Cached API responses
When NOT to use KV:
- Real-time counters or rate limiters (use Durable Objects)
- Data requiring strong consistency (use D1 or Durable Objects)
- High-write workloads (>1 write/second for the same key)
Setting Up Workers KV
Step 1: Create a KV Namespace
# Create production namespace
wrangler kv namespace create "SITE_CONFIG"
# Create preview namespace for local development
wrangler kv namespace create "SITE_CONFIG" --preview
Both commands output a namespace ID. Add them to your wrangler.toml:
[[kv_namespaces]]
binding = "SITE_CONFIG"
id = "abc123def456..."
preview_id = "xyz789ghi012..."
Step 2: Basic CRUD Operations
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// CREATE / UPDATE
await env.SITE_CONFIG.put("greeting", "Hello from the edge!");
// READ
const greeting = await env.SITE_CONFIG.get("greeting");
// READ with type
const data = await env.SITE_CONFIG.get("user:123", { type: "json" });
// DELETE
await env.SITE_CONFIG.delete("old-key");
// LIST keys with prefix
const keys = await env.SITE_CONFIG.list({ prefix: "user:" });
return new Response(JSON.stringify({ greeting, keys }));
},
};
Step 3: Working with Metadata and Expiration
// Store with metadata and TTL
await env.SITE_CONFIG.put("session:abc", JSON.stringify(sessionData), {
expirationTtl: 3600, // Expire in 1 hour (seconds)
metadata: {
userId: "user_123",
createdAt: Date.now(),
},
});
// Read key with its metadata
const { value, metadata } = await env.SITE_CONFIG.getWithMetadata(
"session:abc",
{ type: "json" }
);
Production Patterns
Feature Flags
interface FeatureFlags {
darkMode: boolean;
newCheckout: boolean;
maintenanceMode: boolean;
}
async function getFeatureFlags(env: Env): Promise<FeatureFlags> {
const flags = await env.SITE_CONFIG.get("feature-flags", { type: "json" });
return flags ?? { darkMode: false, newCheckout: false, maintenanceMode: false };
}
Caching API Responses
async function getCachedData(env: Env, apiUrl: string): Promise<unknown> {
const cacheKey = `cache:${apiUrl}`;
const cached = await env.SITE_CONFIG.get(cacheKey, { type: "json" });
if (cached) return cached;
const response = await fetch(apiUrl);
const data = await response.json();
await env.SITE_CONFIG.put(cacheKey, JSON.stringify(data), {
expirationTtl: 300, // 5-minute cache
});
return data;
}
Comparison
| Feature | Workers KV | D1 (SQLite) | Durable Objects | R2 (Storage) |
|---|---|---|---|---|
| Consistency | Eventual | Strong | Strong | Eventual |
| Read latency | ~1-5ms | ~5-30ms | ~5-50ms | ~10-50ms |
| Max value size | 25 MB | Row-based | Unlimited | 5 TB |
| Pricing model | Per operation | Per query | Per request + duration | Per operation + storage |
| Best for | Config, cache | Relational data | Real-time state | Files, media |
Real-World Scenario
You run a multi-language SaaS with users across 40 countries. Your translation strings are stored in a database, but fetching them on every request adds 150ms of latency. By loading translations into Workers KV with language-based keys (i18n:en, i18n:es, i18n:de), each user gets their translations from the nearest edge node in under 5ms. When translators update strings, a webhook writes the new translations to KV, and they propagate globally within 60 seconds.
Gotchas and Edge Cases
- Write frequency: Avoid writing the same key more than once per second in production. KV is optimized for reads, not writes
- Eventual consistency: After a write, other edge locations may serve stale data for up to 60 seconds. Design your application to handle this
- List pagination:
list()returns a maximum of 1,000 keys per call. Use thecursorreturned to paginate through large key sets - Cold reads: The first read of a key at a given edge location fetches from the central store (~100ms). Subsequent reads are from the local cache (~1-5ms)
- Namespace limits: Free tier allows up to 100 namespaces. Organize keys with prefixes instead of creating many namespaces
Troubleshooting
- Error: KV namespace not found: Verify the namespace ID in
wrangler.tomlmatches the one fromwrangler kv namespace list - Stale data after write: Expected behavior due to eventual consistency. For immediate reads after writes, consider using
cacheTtl: 0on subsequent reads - Exceeded daily limits: Free tier caps at 100K reads/day and 1K writes/day. Upgrade to a Workers paid plan for higher limits
- Large value errors: Values cannot exceed 25 MB. For larger data, use Cloudflare R2 (object storage) and store the R2 key in KV
Summary
- Workers KV provides globally distributed key-value storage with sub-5ms read latency from 300+ edge locations
- It is eventually consistent — writes propagate within 60 seconds, making it best for read-heavy workloads
- Use KV for feature flags, caching, i18n strings, and configuration — not for real-time counters or strongly consistent data
- Keys support metadata and TTL expiration for session management and cache invalidation
- Free tier includes 100K reads/day, 1K writes/day, and 1 GB storage
- For strong consistency needs, use D1 (SQLite at the edge) or Durable Objects instead