# Cloudflare Durable Objects: state that lives in one place

> What Durable Objects are and how to use them, with their built-in SQLite storage. A simple counter and a rate limiter, explained from scratch.

Author: Flavio Copes | Published: 2026-06-27 | Canonical: https://flaviocopes.com/cloudflare-durable-objects/

This is the most powerful tool on the Cloudflare platform, and the one that takes a minute to click. Let me try to make it simple.

A **Durable Object** is a tiny server that exists once per name. It keeps its own memory, has its own little database, and handles one request at a time.

That last part is the magic. Because there's only one of each, and it does one thing at a time, you never get two requests stepping on each other. No race conditions.

Think of a chat room. You want exactly one place that holds the messages and the list of who's online. A Durable Object named after the room is that one place.

If you're wondering whether you actually need a Durable Object instead of KV, D1 or R2, I built a free [Cloudflare storage chooser](https://flaviocopes.com/tools/cloudflare-storage-chooser/) that helps you pick.

Let's build the simplest possible one: a counter.

## A counter

A Durable Object is a class. It extends `DurableObject`, and it gets its own SQLite database through `this.ctx.storage.sql`:

```js
import { DurableObject } from 'cloudflare:workers'

export class Counter extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env)
    this.ctx.storage.sql.exec(
      'create table if not exists counter (value integer)'
    )
  }

  async increment() {
    this.ctx.storage.sql.exec(
      'insert into counter (value) values (1)'
    )
    const { count } = this.ctx.storage.sql
      .exec('select count(*) as count from counter')
      .one()
    return count
  }
}
```

The constructor creates the table once. `increment` adds a row and returns the new total. This database belongs to this one object, and it survives restarts.

## Wire it up

Declare the class in `wrangler.jsonc`, and tell Cloudflare it uses SQLite storage with a migration:

```jsonc
{
  "durable_objects": {
    "bindings": [
      { "name": "COUNTER", "class_name": "Counter" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Counter"] }
  ]
}
```

## Use it from a Worker

Here's where the "once per name" idea shows up. You turn a name into an id, then get that object:

```js
export default {
  async fetch(request, env) {
    const id = env.COUNTER.idFromName('global')
    const counter = env.COUNTER.get(id)

    const value = await counter.increment()

    return Response.json({ value })
  },
}
```

`idFromName('global')` always points to the same object. So everyone hitting this Worker talks to one shared counter, and it counts correctly even with lots of people at once.

Notice you call `counter.increment()` directly, like a normal method. Cloudflare routes the call to the object for you, wherever it lives.

If you used a different name, say a user id, you'd get a separate object per user, each with its own count. That's the pattern: one object per *thing* you want to coordinate.

## A real example: rate limiting

Counters are the toy version. A common real use is rate limiting, stopping someone from hammering your API.

One Durable Object per user, holding how many requests they've made recently:

```js
import { DurableObject } from 'cloudflare:workers'

export class RateLimiter extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env)
    this.ctx.storage.sql.exec(
      'create table if not exists hits (at integer)'
    )
  }

  async check() {
    const now = Date.now()
    const windowStart = now - 60_000

    this.ctx.storage.sql.exec('delete from hits where at < ?', windowStart)
    this.ctx.storage.sql.exec('insert into hits (at) values (?)', now)

    const { count } = this.ctx.storage.sql
      .exec('select count(*) as count from hits')
      .one()

    return count <= 10
  }
}
```

`check` drops old hits, records the new one, and returns whether the user is under 10 requests per minute. Because each user has their own object handling one call at a time, the count is always right.

From the Worker:

```js
const id = env.RATE_LIMITER.idFromName(userId)
const limiter = env.RATE_LIMITER.get(id)

if (!(await limiter.check())) {
  return new Response('Too many requests', { status: 429 })
}
```

## When to use one

Reach for a Durable Object when you need *one* place to coordinate something: a chat room, a live document, a game lobby, a counter, a rate limiter.

When the data is just rows you query, use D1. When it's "one authoritative thing that many requests touch at once," that's a Durable Object. The full reference is in the [Durable Objects docs](https://developers.cloudflare.com/durable-objects/).
