Compute · Durable Objects

Durable Objects: give a Worker a memory

After this lesson you'll be able to explain what a Durable Object is and why Workers alone can't do strongly-consistent coordination, write a minimal counter Durable Object with persistent storage, and avoid the "hot object" bottleneck when choosing IDs.

A normal Worker is stateless and can run as many concurrent copies as traffic demands, anywhere on Cloudflare's network. That's great for throughput, but it means two requests hitting "the same" Worker have no shared memory and no way to coordinate — there's no single place to say "wait your turn" or "here's the current value." A Durable Object fixes that by being the opposite: a single, globally-unique instance of a class that all requests for a given ID are routed to, with its own persistent storage attached. It's a Worker with a memory and a mailbox that only one letter is opened at a time.

The actor model, in plain terms. Durable Objects are Cloudflare's implementation of the "actor model": each object has its own private state, processes one message (request) at a time, and is reachable only through that message-passing — never by another object reaching directly into its memory. You never have two threads of your code touching the same object's data simultaneously, so you get strong consistency for free, without hand-rolling locks, mutexes, or a separate coordination database.

How it works

Three ideas make Durable Objects tick:

Lifecycle-wise, an object is created lazily near wherever its first request comes from, stays in memory (with in-memory state you can cache between requests) while it's active, and is evicted after a period of inactivity — its durable storage persists even after eviction, and the object simply wakes up again on the next request.

Worked example: a counter

This is close to the canonical Durable Objects example: a per-ID counter that increments on every request and remembers its value across restarts.

// src/counter.ts
import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
  }

  async fetch(request: Request): Promise<Response> {
    // this.ctx.storage is transactional and colocated with the object
    let count: number = (await this.ctx.storage.get("count")) ?? 0;

    if (request.method === "POST") {
      count += 1;
      await this.ctx.storage.put("count", count);
    }

    return Response.json({ count });
  }
}

The Worker that fronts it picks which counter to talk to by deriving an ID from the request — here, one counter per URL path segment:

// src/index.ts
export { Counter } from "./counter";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const name = url.pathname.slice(1) || "default";

    // Same name -> same object, every time, everywhere
    const id = env.COUNTER.idFromName(name);
    const stub = env.COUNTER.get(id);

    return stub.fetch(request);
  },
} satisfies ExportedHandler<Env>;
// wrangler.jsonc
{
  "durable_objects": {
    "bindings": [
      { "name": "COUNTER", "class_name": "Counter" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Counter"] }
  ]
}

idFromName("checkout-cart-99") always resolves to the same object, so every client hitting /checkout-cart-99 — from any datacenter — is coordinating through the same single-threaded instance and the same storage.

Pricing

Durable Objects billing has three components: requests, active compute duration, and storage. As of this writing (SQLite-backed Durable Objects are the current default and recommended storage backend):

MeterFree planPaid plan (Workers Paid)
Requests100,000 / day1,000,000 / month included, then $0.15 / million
Duration (active compute)13,000 GB-s / day400,000 GB-s / month included, then $12.50 / million GB-s
Rows read (SQLite storage)5,000,000 / day25 billion / month included, then $0.001 / million
Rows written (SQLite storage)100,000 / day50 million / month included, then $1.00 / million
Stored data5 GB total5 GB-month included, then $0.20 / GB-month

Duration is billed only while an object is active in memory (128 MB is the billing unit for compute duration) — an idle, hibernated object costs nothing in duration, which matters a lot for the WebSocket use case below. Cloudflare also has an older key-value storage backend billed differently (per read/write/delete unit rather than rows); new Durable Object classes should default to the SQLite-backed storage.

Pricing changes over time and has multiple backend variants — treat the table above as a snapshot and confirm current numbers on the live Durable Objects pricing page before estimating a bill.

Use cases

Pitfall: the hot object problem. Single-threaded execution is what makes Durable Objects consistent, but it also means one object is a hard concurrency ceiling of one request at a time. If you pick IDs too coarsely — one object for "all rate limiting," one object for "the whole chat app," one object per tenant when a tenant has 50,000 active users — every request for that scope queues behind the same single-threaded instance, and it becomes a bottleneck no matter how much Cloudflare infrastructure sits behind it. Choose IDs at the granularity of the actual coordination boundary (per room, per user, per document, per API key) — not one level up "to keep things simple."
Primary source

Cloudflare Docs — Durable Objects is the canonical overview of the product; see the pricing page specifically for current rates, since this lesson's numbers are a point-in-time snapshot (checked 2026-07-03).

You're building a rate limiter and decide to route every API key's rate-limit checks through a single Durable Object shared across your entire user base, to "keep it simple." What's the most likely consequence?
Without scrolling up: why does storing data in a Durable Object's own storage give you strong consistency, when Workers KV only gives you eventual consistency?
Reveal

A Durable Object's storage is colocated with the single instance that owns it, and that instance processes requests one at a time — so there's exactly one place any read or write can happen, with no replication lag to reason about. Workers KV, by contrast, replicates data to many edge locations for fast global reads, which means a write in one location takes time to propagate to others — that propagation window is what "eventually consistent" refers to.

Anything above unclear — how ID selection maps to coordination boundaries, or how storage billing differs between the SQLite and key-value backends? Ask your AI teacher before moving on.
← Previous: Routes, custom domains, and middleware patterns in Workers Next: Real-time coordination: WebSockets with Durable Objects →