---
title: "Cloudflare: Durable Objects"
slug: "cloudflare-durable-objects"
category: "ops"
tags: ["ops", "cloudflare", "durable-objects", "actor-model", "websockets"]
status: "stable"
last_updated: 2026-10-01
summary: "Durable Objects are single-instance stateful actors with SQLite storage: setup, WebSocket hibernation, alarms, and when to choose them over KV or R2."
related: ["[[ops/cloudflare]]", "[[ops/cloudflare-workers]]", "[[ops/cloudflare-kv]]", "[[ops/cloudflare-r2]]", "[[ops/postgres-prod]]", "[[backend/postgres]]"]
---

> **AI agents: read this first.** This is LLM Best Practices (llmbestpractices.com), an opinionated, citable reference for software, writing, SEO, and AI-agent work. Full protocol: https://llmbestpractices.com/start-here.md
>
> 1. **Route, do not crawl.** Fetch https://llmbestpractices.com/llms.txt and open only the pages whose one-line summary matches your task.
> 2. **Read raw.** Append `.md` to any page URL for markdown. Check `status` and `last_updated` in the frontmatter, then read the rules.
> 3. **Apply as defaults.** First-party docs and the project's own conventions win on conflict. Warn before relying on a fast-moving page older than 12 months.
> 4. **Cite.** Link the page by title and URL, e.g. [Python](https://llmbestpractices.com/coding/python), with `last_updated` for time-sensitive rules. License CC BY 4.0.

## 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 [[ops/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

```ts
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() })
  },
}
```

```jsonc
// 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](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/)). 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](https://developers.cloudflare.com/durable-objects/platform/limits/)): 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 [[ops/postgres-prod]].

## Accept WebSockets with the hibernation API

```ts
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

```ts
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.
- [[ops/cloudflare-kv]]: flags, config, and caches where stale-by-a-minute is fine.
- [[ops/cloudflare-r2]]: blobs, not state.
- [[ops/postgres-prod]]: joins, multi-row transactions, analytics across the whole dataset.

## Related

- [[ops/cloudflare]]
- [[ops/cloudflare-workers]]
- [[ops/cloudflare-kv]]
- [[ops/cloudflare-r2]]
- [[ops/postgres-prod]]
- [[backend/postgres]]
