Cloudflare KV, D1, or Durable Objects
19 Oct 2026KV, D1, and Durable Objects solve different problems. Picking the wrong one means either eventual-consistency bugs you didn’t expect, or a bill for consistency you didn’t need.
The one-line version of each
- KV — a global key-value store, eventually consistent, read-optimized. Great for data that’s written rarely and read constantly.
- D1 — a real SQLite database, per-region with read replicas. Great for relational data with actual queries: joins, filters, counts.
- Durable Objects — a single-threaded, strongly consistent object with its own storage, pinned to one location. Great for coordination: counters, locks, WebSocket session state, anything where two requests racing each other would break correctness.
Decision flow
flowchart TD
A["Need to store data?"] --> B{"Written often,\nread by many?"}
B -->|"Write-heavy"| C{"Need strict\nconsistency?"}
B -->|"Read-heavy,\nrarely changes"| D["KV"]
C -->|"Yes - counters,\nlocks, sessions"| E["Durable Objects"]
C -->|"No - just need\nqueries/relations"| F["D1"]
Where each one breaks if you pick wrong
KV for session data. KV writes propagate globally but aren’t instant — a write in one region can take time to be visible everywhere (Cloudflare’s own docs describe this as eventual consistency, typically settling within seconds, not guaranteed). If you write a session token in one request and read it in the next request routed to a different colo, you can get a stale read. You’ll see intermittent “session not found” errors that don’t reproduce locally, because local dev doesn’t have geographic propagation delay.
D1 for a global counter. D1 is per-database, not a distributed counter. Concurrent writes from different regions to the same row will serialize through SQLite’s locking, which means high write concurrency on a single row turns into a queue. You’ll see increased latency under load, not corruption — but it’s not built for a “like button with a million concurrent clicks” pattern.
Durable Objects for bulk analytics reads. A Durable Object is single-threaded and pinned to one location. If you route heavy read traffic from around the world to a single DO, every one of those reads waits behind the same execution queue and pays the round-trip to that DO’s location. You’ll see rising latency as traffic grows, and it won’t shard on its own — you’d need to manually partition data across multiple DOs.
A concrete mapping
| Use case | Store |
|---|---|
| Feature flags, config | KV |
| Rendered page cache | KV |
| User accounts, orders, relational data | D1 |
Anything with WHERE, JOIN, or COUNT |
D1 |
| Real-time chat room state | Durable Objects |
| Rate limiter / distributed lock | Durable Objects |
| WebSocket connection coordination | Durable Objects |
The practical rule
Start with D1 for anything that looks like normal app data — it’s the one that behaves like what you already know from Postgres or SQLite. Reach for KV only when you’ve confirmed the access pattern is read-heavy and can tolerate a few seconds of staleness. Reach for Durable Objects only when correctness under concurrent access is the actual requirement, not just “this data changes a lot” — that’s still D1’s job.