Durable Objects vs. D1: Choosing the Right State Layer for Your Cloudflare App

I have 60 Cloudflare Workers in production and I’ve made the wrong choice between Durable Objects and D1 exactly once — it cost me two days of refactoring, which is cheap tuition for a lesson that now shapes every architectural decision I make on the edge. The confusion is understandable: both technologies persist state, both live at the edge, and both are positioned as solutions to the “stateless Worker” problem. But they solve fundamentally different problems, and choosing the wrong one doesn’t just make your code awkward — it makes your cost model and your correctness guarantees both wrong.

What D1 Actually Is

D1 is SQLite at the edge — a relational database you query with standard SQL, replicated globally by Cloudflare, accessed via a binding in your Worker. It behaves like a database because it is a database. You write a query, you get rows back, you have transactions, foreign keys, indexes, and all the relational primitives you’d expect. The replication model means reads can be served from the nearest replica and writes go to the primary with propagation to replicas on a short delay — which is the standard read-your-writes trade-off that any distributed database makes. D1 is the right tool for any workload that looks like a database workload: multi-tenant CRUD applications, analytics queries over structured records, any situation where you have a schema and you want to query it with SQL. In my fleet, FinRec — the personal finance PWA I built and described in detail in my post on building a finance PWA on Cloudflare Workers — runs entirely on D1. Every transaction record, every account, every budget category lives in a D1 database with a proper schema. The queries are straightforward SQL: SELECT transactions WHERE account_id = ? AND date > ? ORDER BY date DESC. D1 handles that workload with sub-10ms query times and no coordination complexity whatsoever.

What Durable Objects Actually Are

A Durable Object is not a database — it’s a stateful actor. A single instance of a Durable Object exists at exactly one location in Cloudflare’s network at any given time, and all requests to that object are serialized through that single instance. That serialization guarantee is the entire point: it means you can do things that are impossible with a replicated database, like maintaining an exactly-once counter, coordinating real-time state between multiple WebSocket connections, or enforcing a rate limit that is globally consistent rather than per-region consistent. The storage that a Durable Object has access to is key-value storage local to that instance — fast, strongly consistent, but not SQL, not queryable in aggregate, and not shareable across object instances. You don’t query a Durable Object the way you query D1; you send it a message and it responds, maintaining its own internal state between messages. In my fleet, the career intelligence dashboard at jobs.groundedintelligences.com uses Durable Objects for rate limiting inbound API calls — each rate limiter is a Durable Object instance keyed to a specific API client, and because all requests from that client route to the same object instance, the counter is always accurate with no race conditions. The OSINT monitoring system in my fleet uses KV for caching external feed responses and D1 for structured event storage — cert transparency log entries, threat feed matches, and alert history all go into D1 with timestamps and source metadata, making them queryable for trend analysis and auditing.

The Key Distinction and the Cost Reality

The cleanest mental model for choosing between them: D1 is a database you query; a Durable Object is a stateful actor you message. If your use case involves retrieving or storing structured records that multiple users or processes access independently, you want D1. If your use case involves coordinating state across concurrent requests where order and consistency matter at the request level — not just the transaction level — you want Durable Objects. The cost difference reinforces this. D1 charges per query at very low rates — roughly $0.001 per million read queries on the paid plan, with generous free tier allocations. Durable Objects charge per incoming request and per second of active duration — which means a long-running WebSocket connection or a frequently-polled coordination object accumulates duration costs that a D1 query workload never would. For short-lived stateful operations like rate limiting or session coordination, Durable Objects are cost-effective. For always-on, high-request-volume coordination, the duration billing adds up in ways that aren’t immediately obvious when you’re designing the system.

The Mistake Most Builders Make

The most common error I see in Cloudflare architecture discussions is reaching for Durable Objects when D1 would be correct, because Durable Objects sound more powerful and technically interesting. They are more complex — which is not the same thing as more powerful for every problem. If you need to store and retrieve user records for a SaaS application, Durable Objects give you single-instance serialization and key-value storage for a problem that just needs a relational database. You’ll end up implementing your own query layer on top of Durable Object storage, fighting the absence of joins and indexes, and paying duration costs for object instances that are essentially just holding data that D1 would serve faster. This was the mistake I made in the project that cost me two days of refactoring — I used Durable Objects for structured data storage because I was experimenting with the technology, and I spent those two days rewriting to D1 once the query requirements became clear. The technology is genuinely impressive and there are workloads it handles better than anything else — real-time collaborative editing, game state, presence systems, exactly-once processing pipelines. Those are real use cases. Storing user profile records is not one of them.

What Transfers

The stateful actor versus relational database distinction exists in every distributed system — this is not a Cloudflare-specific decision. Erlang has had the actor model since the 1980s. Akka brings it to the JVM. Orleans brings it to .NET. Every implementation of the actor pattern makes the same trade-off: strong per-actor consistency and message serialization, at the cost of aggregate queryability and shared state. The relational database makes the opposite trade-off: powerful query capabilities and shared state, at the cost of per-record coordination guarantees. Understanding which trade-off your workload actually requires is the architectural judgment call — and it’s easier to make clearly when you’re not choosing between two products on a pricing page but instead asking the underlying question: do I need to query this data, or do I need to coordinate around it?

Building something at the intersection of AI, edge computing, and behavioral science? Let’s connect.

Comments

Leave a Reply

Discover more from Grounded Intelligences

Subscribe now to keep reading and get access to the full archive.

Continue reading