Overview

Workers KV is a globally replicated key-value store tuned for reads at the edge. Reads are fast once a value is cached in a location; writes propagate slowly. Treat it as a cache for configuration-shaped data, not as a database.

Never rely on read-your-writes

Per How KV works, changes “may take up to 60 seconds or more” to be visible in other locations, and Cloudflare advises against relying on immediate visibility even in the location where the write happened. Negative lookups (key not found) are cached too.

  • Do not write a value and read it back from another region in the same flow.
  • Do not use KV for counters, balances, locks, or rate limiters. Use a Durable Object; if you need write-after-write consistency with fast reads, route writes through a Durable Object and read from KV.
  • Use KV where stale-by-a-minute is fine: feature flags, config blobs, long-TTL session data, cached API responses, asset metadata.

cacheTtl on get() defaults to 60 seconds and must be at least 30; raising it improves read speed and lengthens staleness.

Design for reads; throttle writes

  • One write per second per key. More returns 429; retry with exponential backoff.
  • Limits: key 512 bytes, value 25 MiB, key metadata 1,024 bytes, expirationTtl at least 60 seconds.
  • Free plan: 100,000 reads and 1,000 writes, deletes, and list requests per day, 1 GB storage. Paid: 10 million reads and 1 million writes per month included, then 5.00 per million writes (pricing).
  • Writes cost 10 times what reads cost, so batch them and keep them out of hot request paths.

A write-heavy or strongly consistent workload is a signal to move to a Durable Object, D1, or postgres-prod, not to tune KV harder.

Bind the namespace and cache with a TTL

export default {
  async fetch(req: Request, env: { CACHE: KVNamespace }) {
    const cached = await env.CACHE.get("home:hero", { type: "json" })
    if (cached) return Response.json(cached)
    const fresh = await fetchHero()
    await env.CACHE.put("home:hero", JSON.stringify(fresh), { expirationTtl: 3600 })
    return Response.json(fresh)
  },
}

Declare the namespace under kv_namespaces in the Wrangler config (cloudflare-workers). expirationTtl is seconds from now; expiration is an absolute Unix timestamp. Expired keys read as null.

Batch with bulk operations

  • Bulk read: env.CACHE.get(["a", "b", "c"]) takes up to 100 keys, returns a Map, and counts as one operation against the 1,000-operations-per-invocation limit.
  • Bulk write: the REST bulk endpoint accepts up to 10,000 pairs (100 MB per request); from the CLI use npx wrangler kv bulk put <file.json> --binding CACHE (versions before 3.60 use kv:bulk).

Name keys with a delimiter and list by prefix

KV keys are flat strings. Use a consistent delimiter (session:abc123, flag:checkout-v2) so list({ prefix: "session:" }) can enumerate a slice; list returns pages of up to 1,000 keys. Hash or sanitize user input before using it in a key. Composite keys do not turn KV into a relational store; use D1 for joins and range scans.