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

FeatureWorkers KVD1 (SQLite)Durable ObjectsR2 (Storage)
ConsistencyEventualStrongStrongEventual
Read latency~1-5ms~5-30ms~5-50ms~10-50ms
Max value size25 MBRow-basedUnlimited5 TB
Pricing modelPer operationPer queryPer request + durationPer operation + storage
Best forConfig, cacheRelational dataReal-time stateFiles, 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 the cursor returned 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.toml matches the one from wrangler kv namespace list
  • Stale data after write: Expected behavior due to eventual consistency. For immediate reads after writes, consider using cacheTtl: 0 on 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