Strongly-consistent state with Durable Objects
When state MUST be correct, route every request to one instance that handles them one at a time — no locks, no race conditions, no lost updates.
What are we wiring up?
Sometimes you need exactly one place in the whole world to hold a piece of state — a counter that must never miscount, a rate limiter, a game room, or a live document. A Durable Object (DO) gives you that: one instance, addressed by a name, that processes requests one at a time and carries its own storage.
In this guide we wire a browser front-end to a Worker, and the Worker forwards every request to one shared Counter Durable Object. Because all traffic for a given name funnels into the same single instance, increments can never collide.
Think of it like…
One ticket counter for a popular event. No matter how many people show up, there is a single clerk handing out numbered tickets one at a time. Nobody gets the same number, and the count is always right — because there is exactly one clerk keeping the tally.
The pieces & why DO beats KV here
There are four moving parts. The Worker is the front door; the id is the address of your object; the stub is a remote-control handle; and the instance is the one real object with its own storage.
Worker (front door)
A normal stateless Worker that receives the HTTP request and decides which object to forward it to.
id (the address)
idFromName("global") turns a human name into a globally unique ID. Same name always maps to the same instance.
stub (remote control)
env.COUNTER.get(id) returns a stub. You call stub.fetch() locally; Cloudflare routes it to the real object.
instance (one object)
The single live Counter, with its own private storage, that actually mutates and persists the value.
Why not just use Workers KV? KV is eventually consistent: a write spreads to edge copies over time, so two readers in different regions can briefly see different values. For a counter that is fatal — both could read 5, both write 6, and you lose a count. A Durable Object is strongly consistent: there is only one copy, and reads always see the latest write.
Rule of thumb
Use KV when stale reads are fine (config, cached HTML). Use a Durable Object when many writers must agree on one correct, live value.
One at a time: no lost updates
Here is the key property: a Durable Object processes incoming requests serially — one fully finishes before the next begins. So even if Client A and Client B hit the counter at the very same millisecond, the object lines them up and runs read → increment → write twice in order. The final value is 2, never 1.
You get a lock for free
With a normal database you would write a transaction or a lock to stop two requests from clobbering each other. A Durable Object's single-threaded, one-at-a-time execution gives you that mutual exclusion automatically — no locking code to get wrong.
Build it: front-end + Worker + object storage
Three layers, one file for the back-end. The Worker (front door) and the Counter class (instance + storage) live in src/index.js; wrangler.toml binds them together; and a tiny browser snippet calls the API. Note the data layer here IS the object's own storage — there is no separate database to set up.
Create a Worker project
Scaffold a fresh project. We will replace the generated source with our Counter.
npm create cloudflare@latest -- counter-app cd counter-appWrite the Worker + Durable Object
The Worker resolves the id by name and forwards the request; the Counter class reads, increments, and persists its value. See the full src/index.js below.
Bind & migrate in wrangler.toml
Add the binding (the name your Worker uses) and a migration (tells Cloudflare Counter is a new SQLite-backed class).
Deploy
Publish to Cloudflare's network. The instance for "global" is created automatically on first use.
npx wrangler deployTest it
Hit it a few times — the number goes up by exactly one each call, no matter who calls.
curl -X POST https://counter-app.<your-subdomain>.workers.dev/ # 1 curl -X POST https://counter-app.<your-subdomain>.workers.dev/ # 2
// The Durable Object: one instance holds and mutates the count
export class Counter {
constructor(state, env) {
this.state = state; // gives access to this object's private storage
this.env = env;
}
async fetch(req) {
// Read the current value from THIS object's own storage (0 if unset)
let v = (await this.state.storage.get('v')) || 0;
v++;
// Persist before responding; the next request will read this value
await this.state.storage.put('v', v);
return new Response(v.toString(), {
headers: { 'content-type': 'text/plain' }
});
}
}
// The Worker (front door): forward every request to the ONE instance
export default {
async fetch(request, env) {
// Same name -> same single instance, anywhere on Earth
const id = env.COUNTER.idFromName('global');
const stub = env.COUNTER.get(id);
// Cloudflare routes this call to the real Counter object
return stub.fetch(request);
}
};name = "counter-app"
main = "src/index.js"
compatibility_date = "2025-06-01"
# Bind the COUNTER namespace in env to the Counter class
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
# Register Counter as a new SQLite-backed Durable Object class
[[migrations]]
tag = "v1"
new_sqlite_classes = ["Counter"]<button id="go">+1</button>
<span id="count">0</span>
<script>
document.querySelector('#go').addEventListener('click', async () => {
// Every click hits the ONE shared Counter behind the Worker
const res = await fetch('https://counter-app.example.workers.dev/', {
method: 'POST'
});
document.querySelector('#count').textContent = await res.text();
});
</script>Don't read state from the Worker
The Worker is stateless and runs in many places at once. Keep all reads and writes inside the Durable Object's fetch — that is the only place where one-at-a-time ordering and strong consistency are guaranteed.
Key concepts & when to reach for it
Name
A human-friendly string like "global" or "room-42" you pass to idFromName. Picks which instance you reach.
id
The globally unique address derived from the name. Same name always yields the same id.
Stub
A local handle from .get(id). Calling stub.fetch() transparently reaches the real object.
Single instance
Exactly one live object per name across the whole planet — the single source of truth.
Serialized execution
Requests run one at a time inside the object, so no two can interleave and corrupt state.
Strong consistency
One copy of the data; every read sees the most recent write. No stale values, ever.
Storage
Each object has private, transactional storage via this.state.storage.get / put.
Migration
A wrangler.toml entry that registers, renames, or removes an object class.
Not sure which storage to pick? Walk this quick decision tree.
Pitfalls, limits & pricing
One object = one bottleneck
All traffic for a name funnels through a single instance (~1,000 req/s soft limit). A global counter is a teaching example; in production, shard hot state across many objects — one per room, user, or document.
Common mistakes
- Caching the value in the Worker instead of reading it from the object — the Worker has no shared state.
- Using KV for counters or locks — eventual consistency lets two writers lose an update.
- Forgetting the [[migrations]] entry — the deploy fails until the class is registered.
- Assuming different names share data — "room-1" and "room-2" are completely separate instances.
Free to start
SQLite-backed Durable Objects run on the Workers Free plan with lower limits, so you can build and learn this model without paying. Billing is based on requests, compute duration, and stored data.
Related products
menu_bookOfficial docsopen_in_new