When to Choose Durable Objects Over KV or D1 for Coordination
Durable Objects provide a single-threaded, stateful JavaScript class with strong consistency—ideal for rate limiting, WebSocket coordination, and leader election. Includes a working rate-limiter example and production limits you'll hit.
22 Jul 2026, 23:10 UTC

The Coordination Problem That Keeps Coming Back
You need to enforce a strict per-user rate limit across multiple edge locations. Or you're building a real-time cursor-sharing feature where every connected client must see the same position updates instantly. Maybe you're running a scheduled job that must execute exactly once, even if the Worker restarts mid-run. These aren't storage problems—they're coordination problems. Cloudflare KV offers eventual consistency; D1 gives you SQL but no built-in single-writer guarantee. Durable Objects sit in a different category: a stateful, single-threaded JavaScript class that Cloudflare migrates to follow your traffic, with strong consistency baked in.
What Makes the Programming Model Different
A Durable Object is a JavaScript class exported from your Worker. Each instance has a globally unique ID (either user-provided or generated) and runs in its own single-threaded event loop. In-memory state persists across requests as long as the object stays loaded. The runtime handles persistence automatically via the Durable Object Storage API—key-value today, SQLite (beta) with the durable_object_sqlite compatibility flag. You don't manage connections, replication, or failover; the platform migrates the object to the nearest data center when request patterns shift, typically in under 100 ms.
This model eliminates the "read-your-writes" race conditions that plague distributed caches. When a request hits your object, it sees all prior writes from that same object—no cache invalidation, no stale reads. The trade-off is throughput: one object handles roughly 1,000 sustained requests per second. Beyond that, you shard by designing your ID space (e.g., one object per user, per room, per API key).
Worked Example: Exact-Count Rate Limiter
Suppose you need to enforce 100 requests per minute per API key with zero over-counting. With KV you'd risk double-counting during replication lag. With a Durable Object, the counter lives in memory and persists to SQLite atomically.
// RateLimiter.ts
export class RateLimiter {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise {
const url = new URL(request.url);
const key = url.searchParams.get('key') || 'anonymous';
const now = Date.now();
const windowStart = now - 60_000;
// Atomic read-modify-write via SQLite transaction
const count = await this.state.storage.sql.exec(
`WITH cleaned AS (
DELETE FROM requests WHERE timestamp < ?
)
INSERT INTO requests (key, timestamp) VALUES (?, ?)
RETURNING (SELECT COUNT(*) FROM requests WHERE key = ?) AS cnt`,
[windowStart, key, now, key]
);
const current = count[0].cnt as number;
if (current > 100) {
return new Response('Rate limit exceeded', { status: 429 });
}
return new Response(JSON.stringify({ remaining: 100 - current }),
{ headers: { 'Content-Type': 'application/json' } });
}
}
// wrangler.toml
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiter"
[compatibility_flags]
durable_object_sqlite = true # beta, pin Wrangler version
Deploy with wrangler deploy (requires a paid Workers plan for Durable Objects). The sql.exec call runs inside a transaction—no other request to this object interleaves. The cleaned CTE purges old entries each call, keeping the table small. For production, add an index on (key, timestamp) via a migration script you run once per object ID (see limitations below).
Where It Fits—and Where It Doesn't
- Use Durable Objects when: you need a single writer for a logical entity (user session, collaboration room, leader election), WebSocket hibernation for thousands of idle connections, or RPC-style service bindings between Workers.
- Avoid when: your workload is read-heavy and globally distributed (KV or D1 read replicas scale better), you need multi-object transactions (not supported), or your dataset exceeds ~10 GB per object (switch to D1/R2 with the object as coordinator).
- CPU-intensive work blocks the event loop. Offload image processing, crypto, or large JSON parsing to a separate Worker via service binding (
RpcTarget) or usectx.waitUntilfor fire-and-forget background tasks.
Limitations You'll Hit in Production
Migration pauses: when Cloudflare moves an object, it's unavailable for typically <100 ms. Design idempotent APIs and catch OverloadedException for traffic spikes. SQL schema changes: the beta SQL API has no built-in migration tooling. You must write a one-time script per object ID (e.g., a migrate method you call manually) and pin your Wrangler version to avoid surprise breaking changes. WebSocket hibernation: the runtime terminates idle hibernated sockets after 60 seconds unless you send a keep-alive frame via webSocketKeepAlive.
Verify Before You Commit
- Create a minimal object:
wrangler init --template durable-objectand deploy to a*.workers.devsubdomain. Check cold-start latency and migration logs in the dashboard. - Enable
durable_object_sqliteinwrangler.toml, then runwrangler d1 executeagainst the object's embedded SQLite to inspect schema and query plans. - Load test with
hey -n 10000 -c 100while tailing JSON logs:wrangler tail --format=json. Watch CPU time, request latency, and WebSocket hibernation events. - Calculate cost: Durable Objects bill per GB-second of duration plus storage and requests. An idle object with zero requests costs nothing. Use the Cloudflare pricing page with your projected object count and request volume.
Next Step
If your coordination problem maps to a single-writer entity with sub-10 GB state, spin up the template above and measure. The programming model is surprisingly close to a local class—just one that survives deployments and follows your users. For read-heavy global data, keep KV or D1 in the mix and let the Durable Object handle the writes that require consensus.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.