Choosing a data store
Four stores with genuinely different guarantees: D1 for relational records, Durable Objects for ordering and live state, KV for hot lookups, R2 for files. A decision tree, the capacity limits, and the mistakes that are expensive to undo.
Four stores, four different guarantees. Picking correctly at design time is cheap; discovering the mismatch in production is not.
The comparison that matters
| D1 | Durable Object | KV | R2 | |
|---|---|---|---|---|
| Shape | Relational SQL (SQLite) | One object, own SQLite | Key → value | Object storage |
| Consistency | Strong at primary; replicas may lag | Serialized — one writer at a time | Eventual (seconds) | Read-after-write for new objects |
| Concurrency | Queries execute serially | Single-threaded per object | High, unordered | High |
| Ordering | No inherent ordering guarantee | Strict per object | None | None |
| Query | Full SQL, joins, aggregates | Only via that object | Key lookup only | Key lookup / list |
| Capacity | 10 GB per database, 1 TB per account | 10 GB per object | Large values, high volume | Effectively unbounded |
| Point-in-time recovery | Yes — 30 days | No | No | No |
| Per environment | Yes | Yes | Yes | Yes |
Two rows decide most designs: Ordering and Point-in-time recovery. If you need strict ordering, only a Durable Object gives it. If you need to rewind after a bad write, only D1 does.
D1 — the default
Business records go here. It's a real relational database with migrations, indexes, and transactions, and it's the only store with point-in-time recovery.
The two disciplines that keep it healthy, both covered in Database:
- Index every hot column. Queries execute serially, so one unindexed scan delays every request behind it. That makes a missing index an availability problem, and it degrades as data grows rather than failing loudly on day one.
- Read through the request session so replicas can serve reads while each visitor still reads their own writes.
Capacity to plan against: 10 GB per database, 1 TB per account, 2 MB maximum row size, 100 columns per table, and a 30-second ceiling on any single query. Ten gigabytes is a lot of business records and not much if you're storing blobs — which is what R2 is for.
Durable Objects — ordering and live state
A Durable Object is a single-threaded authority over one piece of state. That property is the entire value: while it processes one request, nothing else touches that object's state, so lost updates and double-bookings are impossible by construction rather than by careful coding.
Use one when:
- Many clients share live state they must all agree on — a chat room, presence, a live auction, a collaborative document.
- Work for one key must be strictly ordered — every event for one order, one customer, one document, processed in sequence.
You declare classes at deploy time; instances are created at runtime on first touch, so hundreds of thousands of rooms per class is normal and idle ones hibernate at no cost. Mechanics in Durable Objects.
Two constraints to design around: an object's storage is reachable only through that object — there are no cross-object queries, so anything you need to search or report on must also be written to D1 — and there is no point-in-time recovery. A Durable Object's state is durable, but you cannot rewind it. State you'd want to restore after a mistake belongs in D1, or should be mirrored there.
KV — hot lookups and flags
Fast key reads, and eventually consistent: a write takes seconds to be visible everywhere. That's the whole trade.
Good: cached computations, feature flags, configuration you flip during an incident, session lookups where a few seconds of staleness is harmless.
Wrong: anything read back immediately after writing, anything requiring a query, anything where two concurrent readers must agree.
The incident-flag case is genuinely valuable — a kill switch in KV changes behavior in seconds without a deploy, which is a meaningfully faster mitigation path than shipping code. See Failure domains.
R2 and assets — files
Files, media, generated documents, exports. Public assets are served from /assets/ through the CDN, automatically cached and immutably keyed, so they're never stale — and replacing one means a new key, not an overwrite.
The rule that saves you later: the app's source tree is capped at 256 MB and is not a file store. Media, datasets, and generated output belong in R2 or private artifacts. Every file in the tree is versioned and synced to every checkout forever, so a large file there is a permanent tax on everyone who works on the app. Details in Files and assets.
Combining them
Real applications use several, and the combinations are conventional enough to name:
The pattern generalizes: the Durable Object makes the decision, D1 records it, KV and the edge cache serve the read traffic. Contention is handled exactly once, in the one place that can handle it correctly, and the expensive read path never touches it.
The expensive mistake
Writing contended rows in plain D1 with no concurrency control:
-- Two requests read version 3, both write. One update is silently lost.
UPDATE bookings SET slot = ? WHERE id = ?
It passes every test you'll write, because tests don't race. Then two users book the same slot. The fixes, in increasing order of strength: a version column with a conditional update returning a conflict, a transactional operation route that enforces the invariant in one place, or a Durable Object per resource. Consistency and correctness works through all three.
Next
- Consistency and correctness — the guarantees each store actually provides.
- Durability, backup and recovery — what can be restored, and what cannot.
- Database — the hands-on guide.
Frequently asked questions
What's the default choice?
D1, the relational database. Records, catalogs, orders, users, audit rows — anything you'll query, join, or report on. Reach for another store only when D1's guarantees are the wrong shape for the problem.
When do we actually need a Durable Object?
When many clients share live state they must all agree on, or when work for one key must happen in strict order. A booking system that must never double-book is the canonical case; a chat room is the obvious one.
Is KV a faster database?
No — it's a different guarantee. KV is eventually consistent, so a write can take seconds to be visible everywhere. It's excellent for cached values and feature flags and wrong for anything you need to read back immediately.
How large can these get?
A D1 database holds 10 GB with 1 TB per account; a Durable Object holds 10 GB of SQLite storage; R2 is effectively unbounded. The app's own source tree is capped at 256 MB, which is why files belong in R2 rather than the repository.
Which choice is hardest to reverse?
Putting contended writes in plain D1 rows without version checks. It works in testing and produces lost updates under real concurrency, and by the time you notice, the wrong data is already in production.