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.

The Spikefrost Team30 Jul 20265 min read

Four stores, four different guarantees. Picking correctly at design time is cheap; discovering the mismatch in production is not.

Start at the top. Most application data is relational and belongs in D1 — the other stores answer specific needs D1 is the wrong shape for.

yes

no

yes

no

yes

no

yes

no

What are you storing?

A file or
media object?

R2 + assets
images, video, exports, documents

Must writes for one key
happen in strict ORDER,
or do clients share LIVE state?

Durable Object
booking, inventory, chat room, game

Do you need to query,
join, sort or report on it?

D1
orders, users, catalog, audit rows

Is a few seconds of
staleness acceptable?

KV
cache, feature flags, sessions

Start at the top. Most application data is relational and belongs in D1 — the other stores answer specific needs D1 is the wrong shape for.

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:

A booking system using each store for what only it can do. The Durable Object owns the contended decision; D1 owns the queryable record; KV serves reads that tolerate staleness.

accepted

periodic

Booking request

Durable Object
per resource
serializes the decision

D1
bookings table —
queryable, recoverable

Reports · admin ·
customer history

KV
availability summary

Public availability page
cached, seconds-stale is fine

A booking system using each store for what only it can do. The Durable Object owns the contended decision; D1 owns the queryable record; KV serves reads that tolerate staleness.

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

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.