---
title: "03 — Workers KV: Eventually Consistent Global Key-Value"
uid: workers-kv
tags: ["key-value-store", "kv", "cloudflare", "roadmap:cloudflare", "storage", "edge", "caching"]
excerpt: "KV is an eventually consistent, read-optimized global store — built for config and session data, not source-of-truth records. That one phrase explains every weird behavior."
date: 2026-08-13T03:28:19+0000
source: https://www.aveshina.my.id/en/blog/workers-kv
---

"A key-value store, but at the edge" was my KV summary, and it made the consistency surprises look like bugs. The model that pinned them down: **Workers KV is an eventually consistent, read-heavy global key-value store, optimized for the case where the same value is read far more often than it's written.** [1] Once I internalized _eventually consistent_, every weird behavior fell into a predictable pattern.

The framing that clicked is the access pattern. KV is built for data that's read constantly, written rarely, and where stale-for-a-few-seconds is tolerable [1][3]. Configuration, feature flags, session tokens, precomputed lookup tables — these fit. The current balance of a bank account, an inventory count, anything that must be exactly right at every edge the instant it changes — these do not fit, and that's what Durable Objects or D1 are for.

```figure
<svg viewBox="0 0 740 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Workers KV write-then-propagate model. A Worker writes a value to the central KV store; the value then propagates asynchronously out to multiple edge locations. Reads at any edge return whatever that edge last received, which may briefly lag the central write.">
  <defs>
    <marker id="karrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- center: central store -->
    <rect x="300" y="110" width="140" height="60" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="370" y="135" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">KV central store</text>
    <text x="370" y="152" font-size="9" fill="#475569" text-anchor="middle">authoritative value</text>

    <!-- write arrow -->
    <rect x="40" y="125" width="100" height="40" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="90" y="149" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">Worker · put()</text>
    <path d="M140,145 L298,140" fill="none" stroke="#16a34a" stroke-width="2" marker-end="url(#karrow)"/>
    <text x="219" y="132" font-size="9" fill="#052e16" text-anchor="middle">write</text>

    <!-- edges -->
    <rect x="540" y="40" width="120" height="40" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="600" y="64" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">edge · Singapore</text>
    <rect x="540" y="125" width="120" height="40" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="600" y="149" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">edge · Frankfurt</text>
    <rect x="540" y="210" width="120" height="40" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="600" y="234" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">edge · São Paulo</text>

    <!-- propagation -->
    <path d="M440,130 L538,60" fill="none" stroke="#db2777" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#karrow)"/>
    <path d="M440,140 L538,145" fill="none" stroke="#db2777" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#karrow)"/>
    <path d="M440,150 L538,225" fill="none" stroke="#db2777" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#karrow)"/>

    <text x="600" y="30" font-size="9" font-style="italic" fill="#500724" text-anchor="middle">propagates async — eventually consistent</text>
    <text x="370" y="260" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">a read at a far edge may briefly return the old value until propagation completes</text>
  </g>
</svg>
```

## Key-value operations

The API is deliberately tiny — three operations cover most of what a Worker does with KV [2][4]:

```
await env.MY_KV.put("user:42", JSON.stringify(profile));
const profile = await env.MY_KV.get("user:42");
await env.MYKV.delete("user:42");
```

That's it for the basics. get returns a string by default, or a ReadableStream/ArrayBuffer if you ask for binary. put accepts a string, a stream, or an ArrayBuffer, plus an optional TTL in seconds (expirationTtl) or absolute timestamp (expirationTime) — useful for data that should self-destruct.

The mental note I keep: **every get is a network call** to the nearest edge copy, not an in-memory lookup. It's fast (single-digit milliseconds from a warm edge cache), but it's not free. If a handler reads the same key multiple times, it should read it once into a local variable.

## Metadata handling

Every request carries a standard set of metadata available in the handler — method, URL, headers, CF-specific properties like cf.country (the user's country, derived from their IP) [5]. KV values can carry their own metadata too: put accepts a metadata object, and getWithMetadata returns both the value and its metadata in one call. This is how I attach lightweight context — a content type, a version, a tag — without parsing the value body.

There's also the scheduled event handler, distinct from fetch, which fires on a cron trigger [6]. KV is a natural fit for scheduled jobs: a cron Worker refreshes a cache every five minutes, and reads stay sub-millisecond the rest of the time.

## Bulk operations

For loading or updating many keys at once, KV exposes bulk APIs that batch many operations into a single request [7]:

```
await env.MY_KV.bulkPut([
  { key: "a", value: "1" },
  { key: "b", value: "2" },
]);
await env.MY_KV.bulkGet(["a", "b", "c"]);
```

This is the right tool for initial data import, large cache refreshes, or migration. It avoids the latency of issuing hundreds of individual put calls sequentially. Bulk writes are subject to per-batch size limits, so for very large datasets the pattern is chunked batches in a loop.

## Caching patterns

KV is itself a cache-shaped store, and it pairs with Workers' Cache API to cover two layers [8]:

- **Cache API** — per-edge, in-memory/short-TTL cache of a Response. Fastest, but each edge has its own copy.
- **KV** — global, eventually consistent, longer-lived. One authoritative value propagates to all edges.

The combined pattern for read-heavy data is: check the Cache API first (free, instant if hit); on miss, check KV; on KV miss, fetch from origin and write back to both layers. The Cache API catches the hottest reads at no cost; KV catches the rest globally; origin is the last resort.

The three cache strategies from earlier (cache-first, network-first, stale-while-revalidate) all apply here, and they compose with KV cleanly. Stale-while-revalidate is the one I reach for most: serve whatever the edge has immediately, refresh from origin in the background.

## Where KV fits (and where it doesn't)

The decision that took me longest to internalize was the consistency boundary. KV is **eventually consistent**: after a put, the new value propagates to edge locations over the next ~60 seconds (a Cloudflare-documented bound) [1][3]. During that window, different edges may serve different values. This is fine for a feature flag (every edge gets the new flag within a minute) and unacceptable for a payment (every edge must agree, immediately).

The rule I use:

- **Use KV** for: configuration, feature flags, session lookup, A/B test assignments, rate-limit counters that tolerate drift, cached rendered output, lookup tables that change rarely.
- **Don't use KV** for: anything transactional, anything where two writes must agree immediately, anything where reading a stale value causes real harm.

For the cases KV doesn't cover, the answer is either D1 (SQL, per-database consistency) or Durable Objects (single-actor, strongly consistent), covered in later notes.

## How I use this

The pattern that crystallized is "KV as the first read-through layer." Almost every Worker I write that needs state starts with a KV namespace for its hot reads — config, cached upstream responses, session lookups — and only reaches for D1 or Durable Objects when the consistency requirement actually demands it. Getting the consistency model right up front is the whole game; the API itself is small enough to learn in an afternoon.

## References

[1] Cloudflare, "Workers KV Documentation," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/kv/](https://developers.cloudflare.com/kv/)

[2] Cloudflare, "Read key value pairs," Cloudflare KV API, 2024. [Online]. Available: [https://developers.cloudflare.com/kv/api/read-key-value-pairs/](https://developers.cloudflare.com/kv/api/read-key-value-pairs/)

[3] Cloudflare, "Workers KV Runtime API," Cloudflare Workers Runtime APIs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/kv](https://developers.cloudflare.com/workers/runtime-apis/kv)

[4] Cloudflare, "Cloudflare Workers KV — Cloudflare Docs," Cloudflare Workers Runtime APIs. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/kv/](https://developers.cloudflare.com/workers/runtime-apis/kv/)

[5] Cloudflare, "Request and Response," Cloudflare Workers Runtime APIs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/request](https://developers.cloudflare.com/workers/runtime-apis/request)

[6] Cloudflare, "Scheduled event handler," Cloudflare Workers Runtime APIs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/runtime-apis/scheduled-event](https://developers.cloudflare.com/workers/runtime-apis/scheduled-event)

[7] Cloudflare, "Workers KV is GA," Cloudflare Blog. [Online]. Available: [https://blog.cloudflare.com/workers-kv-is-ga/](https://blog.cloudflare.com/workers-kv-is-ga/)

[8] Cloudflare, "How the cache works · Cloudflare Workers," Cloudflare Docs, 2024. [Online]. Available: [https://developers.cloudflare.com/workers/reference/how-the-cache-works/](https://developers.cloudflare.com/workers/reference/how-the-cache-works/)

```quiz
Q: After a `put()` to Workers KV, when is the new value visible at all edges?
- Immediately, with strong consistency
- Eventually — propagation across edges takes up to about 60 seconds
- Only after a manual cache purge
correct: 1
explain: KV is eventually consistent. A write hits the central store, then propagates to edges asynchronously. During the window, different edges may serve different values.

Q: Which workload is the BEST fit for Workers KV?
- Bank account balances where every edge must agree immediately
- A feature flag read on every request and changed a few times a week
- A transactional write that must commit atomically with another write
correct: 1
explain: KV is built for read-heavy, write-rare data where brief staleness is tolerable. Feature flags, config, and session lookups fit. Transactional and immediately-consistent data does not.

Q: What's the recommended way to read the same KV key twice in one handler?
- Call `get()` twice — it's free since it's in-memory
- Read it once into a local variable, reuse that
- Use `bulkGet()` for duplicates
correct: 1
explain: Every `get` is a network call to the nearest edge copy. It's fast but not free, so cache the result in a local variable within the handler.

Q: How do KV and the Cache API relate?
- They're the same thing under different names
- Cache API is per-edge and short-lived; KV is global and longer-lived — they compose in layers
- KV replaces the Cache API entirely
correct: 1
explain: The Cache API holds the hottest reads at a single edge for free; KV holds the global, eventually-consistent copy. A typical read path checks Cache first, then KV, then origin.

Q: A scheduled (cron) Worker refreshes a KV value every 5 minutes. Reads between refreshes are…
- guaranteed to be exactly the value just written
- served from the cached KV value, sub-millisecond, accepting brief staleness
- rejected until the next refresh
correct: 1
explain: The cron job keeps the KV value fresh on a schedule; reads between runs return the last-written value immediately from the nearest edge. This is the canonical KV usage pattern.
```
