Overview

A Durable Object is an actor: one globally unique ID maps to one running instance, with its own strongly consistent storage and single-threaded execution. Use one when a workload needs serialized state, coordination, or a long-lived WebSocket. Stay on cloudflare-kv when the workload is read-heavy and tolerates stale data.

Use one ID per thing that needs a single writer

Requests for an ID always reach the same instance, which Cloudflare places near the first caller. Coordination collapses to “send a message to this object”: no locks, no leader election. Execution is cooperative and single-threaded, so there are no data races inside an object. The instance is the actor, the name is the address, and the class methods are the handlers.

Define the class, extend DurableObject, call methods over RPC

import { DurableObject } from "cloudflare:workers"
 
export class Counter extends DurableObject {
  async increment(): Promise<number> {
    const count = ((await this.ctx.storage.get<number>("count")) ?? 0) + 1
    await this.ctx.storage.put("count", count)
    return count
  }
}
 
export default {
  async fetch(req: Request, env: Env) {
    const stub = env.COUNTER.getByName("global") // same name, same instance, always
    return Response.json({ count: await stub.increment() })
  },
}
// wrangler.jsonc
{
  "exports": { "Counter": { "type": "durable-object", "storage": "sqlite" } },
  "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }] }
}

Cloudflare’s current config declares classes in exports, with storage of sqlite (recommended) or legacy-kv. Older Workers use a migrations array with new_sqlite_classes for SQLite-backed classes; new_classes creates the legacy key-value backend, so check which one a config uses. A Worker uses one mechanism at a time, and cannot go back to migrations after deploying with exports (migrations docs). Use getByName (or idFromName) for “one per user or room”; use newUniqueId when the object itself is the identity.

Store data in the SQLite backend

Cloudflare recommends SQLite-backed storage for all new namespaces. It adds ctx.storage.sql.exec() for real SQL and point-in-time recovery, and keeps the key-value methods (get, put, delete, list).

  • Writes issued without an intervening await are coalesced and committed atomically. Use transactionSync() for multi-statement SQL; an exception rolls the transaction back.
  • ctx.blockConcurrencyWhile() blocks interleaving while initialization runs.
  • Limits (checked October 2026, limits page): 10 GB per object on paid; on Free, 1 GB per object and 5 GB per account; key plus value up to 2 MB; a soft limit of 1,000 requests per second per object; CPU 30 seconds per request by default, configurable to 5 minutes.
  • The legacy key-value backend remains only for existing paid namespaces (128 KiB values, 50 GB per account).

For cross-object SQL or analytics, use D1 or postgres-prod.

Accept WebSockets with the hibernation API

async fetch(req: Request) {
  const pair = new WebSocketPair()
  this.ctx.acceptWebSocket(pair[1])   // not ws.accept(): this opts into hibernation
  return new Response(null, { status: 101, webSocket: pair[0] })
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
  ws.send(`echo: ${message}`)
}

With acceptWebSocket, the runtime can evict an idle object from memory while the sockets stay open, then wake it on the next message, so many idle connections cost almost nothing. Cloudflare’s limits page lists no per-object connection cap; load-test your own message rate against the 1,000 requests per second soft limit.

Schedule per-object work with alarms

await this.ctx.storage.setAlarm(Date.now() + 60_000)
// ...
async alarm() { await this.flush() }
  • An object holds one alarm at a time; setting a new one replaces it.
  • Alarms run at least once and are retried with exponential backoff (starting at 2 seconds, up to 6 retries) when alarm() throws, so make the handler idempotent.
  • Use them for session expiry, debounced flushes, and retry with backoff. They run even if no client is connected.

Choose by consistency and access pattern

  • Durable Object: counters, balances, rate limiters under contention, chat rooms, game state, leader election.
  • cloudflare-kv: flags, config, and caches where stale-by-a-minute is fine.
  • cloudflare-r2: blobs, not state.
  • postgres-prod: joins, multi-row transactions, analytics across the whole dataset.