Cloudflare Durable Objects: state that lives in one place
By Flavio Copes
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.
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.
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:
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:
{
"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:
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:
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:
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.
Related posts about cloudflare: