sellIntegration
hub

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.

1Instance per name
StrongConsistency
0Lost updates
~1kReq/s per object
insights

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.

schemaMany clients, one instance

idFromName(global)

Client A

Worker

Client B

Client C

DO ID

Counter (one instance)

Storage (v)

support_agent

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.

account_tree

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.

door_front

Worker (front door)

A normal stateless Worker that receives the HTTP request and decides which object to forward it to.

fingerprint

id (the address)

idFromName("global") turns a human name into a globally unique ID. Same name always maps to the same instance.

settings_remote

stub (remote control)

env.COUNTER.get(id) returns a stub. You call stub.fetch() locally; Cloudflare routes it to the real object.

hub

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.

schemaKV (eventual) vs Durable Object (strong)

Write v=6

KV (eventual)

Edge copy 1 (fresh)

Edge copy 2 (stale)

Write v=6

Durable Object (strong)

One value, always fresh

balance

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.

swap_vert

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.

schemaTwo concurrent increments, serialized
"Storage""Counter DO""Client B""Client A""Storage""Counter DO""Client B""Client A"serialized, one at a timePOST /incrementPOST /incrementread v = 00write v = 11read v = 11write v = 22
lock

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.

construction

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.

  1. Create a Worker project

    Scaffold a fresh project. We will replace the generated source with our Counter.

    bash
    npm create cloudflare@latest -- counter-app
    cd counter-app
  2. Write 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.

  3. 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).

  4. Deploy

    Publish to Cloudflare's network. The instance for "global" is created automatically on first use.

    bash
    npx wrangler deploy
  5. Test it

    Hit it a few times — the number goes up by exactly one each call, no matter who calls.

    bash
    curl -X POST https://counter-app.<your-subdomain>.workers.dev/
    # 1
    curl -X POST https://counter-app.<your-subdomain>.workers.dev/
    # 2
jssrc/index.js — Worker + Counter object
// 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);
  }
};
tomlwrangler.toml — binding + migration
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"]
htmlFront-end — one shared counter for everyone
<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>
warning

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.

school

Key concepts & when to reach for it

badge

Name

A human-friendly string like "global" or "room-42" you pass to idFromName. Picks which instance you reach.

fingerprint

id

The globally unique address derived from the name. Same name always yields the same id.

settings_remote

Stub

A local handle from .get(id). Calling stub.fetch() transparently reaches the real object.

looks_one

Single instance

Exactly one live object per name across the whole planet — the single source of truth.

format_list_numbered

Serialized execution

Requests run one at a time inside the object, so no two can interleave and corrupt state.

verified

Strong consistency

One copy of the data; every read sees the most recent write. No stale values, ever.

database

Storage

Each object has private, transactional storage via this.state.storage.get / put.

move_up

Migration

A wrangler.toml entry that registers, renames, or removes an object class.

Not sure which storage to pick? Walk this quick decision tree.

schemaWhich storage do I need?

No, just cache

Yes, must be consistent

Yes

No, lots of relational data

Need shared state?

Use KV

One hot key or coordination point?

Use a Durable Object

Use D1

tips_and_updates

Pitfalls, limits & pricing

balance

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.
savings

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.