Skip to content
The Orange Cloud Report

Is D1 ready for a system of record?

Putting D1 in production

Not for a system of record. The size cap and the single writer are architectural, not roadmap items, and the transient errors are routine rather than exceptional. Use Postgres or MySQL behind Hyperdrive.

Last updated

The products, scored

ProductMy takeScore
KVDead-simple global key-value storage. Design for its consistency model and it's superb.Score: 9/10
HyperdriveMakes an existing database fast from Workers, and does it well.Score: 8/10
D1Good for demos and proofs of concept. Not production.Score: 3/10
Durable ObjectsTechnically impressive, operationally terrifying. Should have stayed an internal primitive.Score: 2/10

D1 is the easiest database on Cloudflare’s platform to start using. It is SQLite, it sits next to your Workers, setup is close to nothing, and the pricing is very low. Everything about the first hour is good. The question worth asking is what happens as you scale, because that is where I think D1 gives out. If you are still deciding between storage products in general, start with storing data on Cloudflare instead. This guide is about the specific question of whether D1 can hold data you cannot afford to lose.

The limits are the architecture, not the roadmap

A D1 database is capped at 10 GB on Workers Paid and 500 MB on Free, with 1 TB of total D1 storage per paid account. In practice the ceiling arrives earlier than the number suggests, since error rates climb well before you reach it.

The limit that matters more is that each database is single-threaded and processes queries sequentially. There is one writer. That is not a quota Cloudflare can raise. Growing past one database means sharding by hand across the 50,000 databases a paid account can hold, and D1 gives you nothing to help with that. You get to write the routing, the cross-shard queries, and the rebalancing yourself. Building a distributed database on top of a product that does not know it is one is a bad trade against just running Postgres.

The smaller limits are worth knowing before you design a schema. 100 columns per table, 2 MB for any single row or value, 100 bound parameters per query, 1,000 queries per Worker invocation on paid plans, and 30 seconds maximum for a single query.

D1 pricing is a row counter

D1 bills rows read and rows written rather than time spent querying. Paid plans include 25 billion rows read and 50 million rows written per month, then charge $0.001 per million rows read and $1.00 per million rows written. Storage is $0.75 per GB-month past the first 5 GB.

What catches people is the definition of a row read. Cloudflare’s wording is that rows read “measure how many rows a query reads (scans), regardless of the size of each row”, and that a query filtering on an unindexed column “may return fewer rows to your Worker, but is still required to read (scan) more rows”. A query that returns one row out of a million can bill a million rows read. On D1 a missing index is not only a latency problem, it is a billing problem, and it grows with your table rather than with your traffic.

The rates are lopsided in a way that decides how you should react. Rows written cost a thousand times more per row than rows read. Every index has to be maintained on write, and Cloudflare is direct that writing an indexed column means writing both the table and the index, while arguing the read benefit “will, in nearly all cases, offset this additional write”. I think that is the right default. But it means over-indexing a write-heavy table loads the expensive meter and under-indexing loads the cheap one, so the two mistakes are not symmetrical and the answer is never to index everything.

Then the two constraints meet. Tables and indexes both count toward storage consumed, and an index is “effectively a table itself”. So the 10 GB ceiling is not 10 GB of your data. It is your data plus every index you added to keep the read billing sane. The mitigation for one problem spends the budget for the other, which is why I would treat any design that lands near the cap as already past it.

Read replication does not fix writes

D1 read replication is real and useful - sometimes. It creates asynchronously replicated read-only copies and routes reads to a closer one. The Sessions API gives you sequential consistency within a session, including read-your-own-writes, which is a better guarantee than most people expect from a replicated setup.

It does not change the write path. Every write is still forwarded to the single primary instance. Replication improves read latency and read throughput. If your problem is write contention, and with a single writer it eventually is, replication is not the answer.

Replicas need traffic to stay useful

Two things have to be true before replication does what you designed it for, and only one of them is documented.

The documented one is that you have to use the Sessions API. Without it, every query goes to the primary no matter how many replicas exist. That trips people up because replication looks like a setting you turn on rather than a change to how you open connections.

The undocumented one matters more. Replicas are Durable Objects, and Cloudflare’s own description of the design has them booting by fetching the latest snapshot and replaying the log from cold storage. That is a cold start. A replica is only fast when it is already resident, and a region needs a steady stream of queries to keep it that way. Cloudflare says it creates a replica in every supported region and says nothing about what happens to one that goes quiet. Given what it is built on, I would not assume a replica in a low-traffic region is sitting warm and waiting for you.

The effect runs in an awkward direction. Busy regions keep their replicas warm and get the latency win. Quiet regions, which are often the ones furthest from your primary and where the saving would be largest, are the least likely to have a warm replica to serve from. So the benefit concentrates where you needed it least, and the tail of users you turned replication on for keeps paying close to the primary’s latency.

There is a ceiling on the other side of that. You get one replica per region, and a replica is a Durable Object with the same single-threaded execution as the primary. The read capacity available in a region is therefore one SQLite instance, not a pool you can grow, and nothing about replication lets you add a second replica where the traffic actually is.

That leaves a fairly narrow band to aim for. You want enough sustained traffic in a region to keep its replica resident, and not so much that a single-threaded replica becomes the same bottleneck you turned replication on to escape. Measure the latency you actually get per region before you count on it, rather than reading the feature list and assuming the benefit lands evenly.

The error handling is yours to write

This is the part I weigh most heavily, and it is why D1 scores a 3. Cloudflare’s own debugging documentation catalogs the transient failures you should expect during normal operation. D1 retries read-only queries up to twice by itself, and the docs put the success rate of those retries anywhere between 5% and 95% depending on the underlying error. Writes get no automatic retry.

There are also no interactive transactions, only an atomic batch(). So a multi-step write that fails halfway is a recovery path you design and implement, without the transaction semantics you would normally reach for. D1 also runs on Durable Objects storage underneath, which is where several of those errors originate. You inherit that platform’s operational behavior whether or not you ever use Durable Objects directly.

None of this makes D1 unusable. It makes D1 a database that pushes reliability work back onto your application code, and that work is easy to underestimate at the point where you are choosing.

D1 vs the alternatives

D1 vs Hyperdrive. This is the real decision for production work. D1 owns the database and removes setup. Hyperdrive connects Workers to a Postgres or MySQL database you own, pooling connections close to it. You keep mature tooling, real transactions, backups you understand, and a path off the platform. Hyperdrive is included with paid Workers plans. For a system of record I would take it every time.

D1 vs managed Postgres. Planetscale and Neon are Postgres, which means extensions, interactive transactions, and vertical scaling. Put either behind Hyperdrive and you get the edge access pattern without the SQLite ceiling. Choosing D1 over these buys you slightly less setup in exchange for a 10 GB wall.

D1 vs Turso. Turso is also SQLite, with embedded replicas for very low read latency, and it keeps the single-writer model. It solves a portability problem D1 does not, but it does not solve the constraint that pushed you to ask this question.

D1 vs KV. If the data is a read-heavy lookup by key and staleness is acceptable, KV is the better product and scores much higher here. People reach for D1 because SQL feels safer than eventual consistency. If you are only ever fetching by key, that instinct is costing you reliability rather than buying it.

Where D1 is the right answer

Prototypes and demos, where the setup cost saved is real and the failure modes do not matter yet. Small read-heavy datasets that fit comfortably under the cap and can be rebuilt from somewhere else. Per-tenant data that is disposable, where the database-per-tenant model plays to the 50,000 database allowance rather than against the single writer.

Anything you would be upset to lose belongs somewhere else today.