Choosing the Right Persistence Layer for Cloudflare Workers: KV, Durable Objects, and D1
A decision guide for choosing between Cloudflare Workers KV, Durable Objects, and D1 based on consistency, scaling, and access patterns, with a concrete validation example.
05 Sept 2026, 16:15 UTC

The Problem: Matching State Requirements to Edge Storage
When building on Cloudflare Workers, you cannot rely on local memory for persistence because Workers are ephemeral and distributed. Choosing the wrong storage layer leads to either data corruption (due to eventual consistency) or performance bottlenecks (due to single-threaded coordination). The key takeaway is to align your consistency requirement with your access pattern.
Decision Matrix: KV vs. Durable Objects vs. D1
Use this table to narrow down your choice based on the primary constraint of your application.
| Feature | Workers KV | Durable Objects | D1 (SQLite) |
|---|---|---|---|
| Consistency | Eventual (Global) | Strong (Per-Object) | Strong (ACID) |
| Best For | Read-heavy config, caching | Real-time state, coordination | Relational data, complex queries |
| Scaling Axis | Global read-edge caching | Number of unique Object IDs | Database size / query complexity |
| Primary Limit | Write propagation delay | Single-threaded per ID | 10 GB per DB (beta) |
| Pricing Driver | Read/Write/List ops | Request duration & storage | Row reads/writes |
Analyzing the Trade-offs
Workers KV: Optimized for Reads
KV is a globally replicated key-value store designed for data written once and read millions of times. Because it replicates data to every edge location, reads are very fast. However, writes are eventually consistent: a change may take seconds to propagate globally. Never use KV for counters, inventory, or authentication tokens where immediate read-after-write consistency is required.
Durable Objects: State Coordination
Durable Objects (DO) provide a unique coordination point. Each object has a unique ID and runs in a single location, ensuring strong consistency for that ID. This suits WebSocket rooms, shopping carts, or per-user rate limiters. The trade-off is the hot-key bottleneck: a single object is single-threaded, so thousands of requests to one ID will queue and increase latency. Scale by sharding across many object IDs (for example, one per user or session).
D1: Relational Structure
D1 is Cloudflare's serverless SQLite database. It is the right choice when you need SQL capabilities—JOINs, indexes, transactions, complex filtering—that KV and DO cannot provide. While it offers ACID transactions, note that as a beta product it has a 10 GB per-database limit and may show higher latency (roughly 10–50 ms per query) than KV reads, since it does not use the same global caching mechanism.
Implementation and Validation
To validate these behaviors, use wrangler dev to emulate the environment locally. You need the Wrangler CLI installed and a Cloudflare account with the resources created (via the dashboard or wrangler kv:namespace create / wrangler d1 create).
1. Configuration (wrangler.toml)
Add bindings to your configuration file. Replace the placeholder IDs with real values from your account.
kv_namespaces = [
{ binding = "MY_KV", id = "<your-kv-namespace-id>" }
]
[[d1_databases]]
binding = "MY_D1"
database_name = "guide_db"
database_id = "<your-d1-database-id>"
[[durable_objects.bindings]]
name = "MY_DO"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
2. Implementation Example
This example Worker handles three state patterns. Durable Objects require a separate exported class.
// Durable Object class: strongly consistent counter
export class Counter {
constructor(state) {
this.state = state;
}
async fetch(request) {
let value = (await this.state.storage.get("count")) || 0;
value++;
await this.state.storage.put("count", value);
return new Response(String(value));
}
}
// Main Worker entry point
export default {
async fetch(request, env) {
const url = new URL(request.url);
// KV: fast read, eventual consistency
if (url.pathname === "/kv") {
return new Response(await env.MY_KV.get("config_key"));
}
// DO: strong consistency for a specific ID
if (url.pathname === "/do") {
const id = env.MY_DO.idFromName("global-counter");
const stub = env.MY_DO.get(id);
return stub.fetch(request);
}
// D1: relational query
if (url.pathname === "/d1") {
const { results } = await env.MY_D1
.prepare("SELECT * FROM users LIMIT 1")
.all();
return Response.json(results);
}
return new Response("Not found", { status: 404 });
}
};
3. Local Verification
Run these commands in your project directory. wrangler dev requires no special permissions beyond your Cloudflare login; curl must be installed.
# Start the local emulator (Miniflare under the hood)
wrangler dev
# In another terminal, test each binding
curl http://localhost:8787/kv
curl http://localhost:8787/do # expect incrementing numbers
curl http://localhost:8787/d1 # requires a 'users' table to exist
Note: local emulation differs from production in latency and consistency behavior. Treat local results as a functional check, not a performance measurement.
4. Production Result Check
After deploying (wrangler deploy), verify real behavior:
- KV consistency: Write a value, then immediately read it from another region (for example via a VPN exit node). You may see the stale value for several seconds—this is expected.
- DO atomicity: Send many concurrent requests to one Durable Object with a load tool such as k6. The final count should equal the exact number of requests, proving no race conditions.
- D1 latency: Compare time-to-first-byte for a D1 query versus a KV read to confirm the latency trade-off for your workload.
Limitations and Risks
- KV: Unsuitable for anything needing a read-after-write guarantee.
- Durable Objects: A hot single ID causes queuing; shard by user or session ID. Each object also has memory and per-request CPU limits—check current documented values before designing around them.
- D1: Beta limits (10 GB per database, limited multi-region replication) may not fit large datasets. Confirm current limits and pricing on the Cloudflare dashboard and developer docs before committing, as beta terms can change.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.