Skip to content
Search
See every article
Cloudflare WorkersWritten by Cipher

Estado consistente en el edge con Durable Objects

Durable Objects te da una instancia única y con estado por cada ID que elijas, corriendo en el mismo datacenter donde vive su storage. Sirve para coordinar clientes, contar cosas sin condiciones de carrera y guardar estado por-usuario sin levantar una base de datos aparte.

La garantía que cambia todo: unicidad global

Antes de escribir una línea de código tenés que entender esto: para un ID dado, Cloudflare garantiza que existe exactamente una instancia de tu Durable Object corriendo en todo el mundo. No hay dos réplicas escribiendo al mismo tiempo, no hay que pelear con locks distribuidos. El runtime enruta cada request al datacenter que 'dueña' ese objeto, y ese objeto ejecuta todo en un solo hilo. Es la razón por la que podés usar Durable Objects para cosas que en otro lado necesitarían Redis con locks, o un Postgres con transacciones explícitas. La contracara es que un objeto es un cuello de botella intencional: si necesitás paralelismo real, la unidad de escalado es el ID, no el objeto individual.

Antes de tocar código: elegí el storage correcto

Cloudflare no te da un solo producto de datos, te da varios, y elegir mal te va a doler en tres semanas. Si necesitás lecturas rapidísimas de datos que casi no cambian (config, sesiones, feature flags), eso es Workers KV: eventualmente consistente, cacheado en el borde, pensado para miles de lecturas por segundo sobre la misma key. Si necesitás coordinación fuerte entre clientes, estado transaccional o algo por-usuario que tiene que ser correcto siempre, eso es Durable Objects. Si es un archivo grande, es R2. La regla práctica: KV para lo que se lee mucho y se escribe poco; Durable Objects para lo que necesita orden estricto y consistencia; R2 para blobs. Una misma app puede perfectamente combinar los tres.

La clase: tu objeto tiene su propia base SQLite

Un Durable Object no es solo una clase con métodos, viene con una base de datos SQLite privada y embebida a la que solo ese objeto puede acceder. Esto es clave: no hay que provisionar nada por separado, cada instancia trae su storage. Los métodos públicos de la clase se exponen automáticamente como RPC, así que el Worker que lo llama no necesita saber de HTTP interno ni serializar mensajes a mano. Fijate que sql.exec() corre directo sobre ctx.storage, que es el handle al SQLite del objeto.

typescript
import { DurableObject } from "cloudflare:workers";
​
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
// Requerido, porque extendemos la clase base.
super(ctx, env)
}
​
async sayHello(): Promise<string> {
let result = this.ctx.storage.sql
.exec("SELECT 'Hello, World!' as greeting")
.one();
return result.greeting;
}
}

El Worker habla con el objeto por su nombre, nunca directo

Un cliente HTTP nunca toca un Durable Object directamente: siempre pasa por un Worker que actúa de interfaz. El Worker construye un 'stub', que es básicamente un cliente apuntando a un ID específico, y ese ID determina qué instancia del objeto (y qué copia de su SQLite) vas a usar. Usar getByName con un string legible (el path, un room ID, un user ID) es la forma más simple: mismo nombre, mismo objeto, siempre. Esto es lo que te permite, por ejemplo, tener un objeto distinto por cada sala de chat o por cada usuario sin gestionar un registro de IDs a mano.

typescript
export default {
async fetch(request, env, ctx): Promise<Response> {
const stub = env.MY_DURABLE_OBJECT.getByName(new URL(request.url).pathname);
​
const greeting = await stub.sayHello();
​
return new Response(greeting);
},
} satisfies ExportedHandler<Env>;

Bindings y storage: declarar el objeto en wrangler

Un Durable Object no existe hasta que lo declarás en dos lugares del archivo de configuración: el binding (cómo lo llama tu Worker) y el export (qué clase es y qué backend de storage usa). El binding le da un nombre a env.MY_DURABLE_OBJECT dentro de tu Worker; el export le dice a Cloudflare que provisione un namespace para esa clase la primera vez que hacés deploy, y que gestione su ciclo de vida en deploys futuros. Elegir storage: sqlite es lo que habilita el SQL API que viste arriba.

json
{
"durable_objects": {
"bindings": [
{
"name": "MY_DURABLE_OBJECT",
"class_name": "MyDurableObject"
}
]
},
"exports": {
"MyDurableObject": {
"type": "durable-object",
"storage": "sqlite"
}
}
}

What you can build with this

Three projects you can finish in an afternoon, each ending in something that runs.

  1. Contador de presencia por sala de chat

    Un Durable Object por sala de chat, identificado con getByName(roomId), que mantiene con SQL API cuántos usuarios están conectados y expone un método RPC para sumar y restar presencia.

    Why it's worth it — Resuelve el problema clásico de contar conexiones concurrentes sin condiciones de carrera, porque el objeto es single-threaded y único globalmente para ese roomId.

  2. Rate limiter por usuario con estado transaccional

    Un Durable Object por userId que guarda en su SQLite privado cuántos requests hizo el usuario en la ventana actual, y un método RPC que decide si el siguiente request se permite o se rechaza.

    Why it's worth it — Evita el problema de rate limiting distribuido con múltiples nodos leyendo y escribiendo el mismo contador sin coordinación, porque acá solo existe una instancia autoritativa por usuario.

  3. Config store de alta lectura combinado con estado crítico

    Una API donde la configuración de features (que se lee mucho y cambia poco) vive en Workers KV, y el estado que necesita ser siempre correcto —como un inventario o un balance— vive en un Durable Object por recurso.

    Why it's worth it — Te enseña a no meter todo en un solo producto de storage: usar KV donde la velocidad de lectura manda y Durable Objects donde la consistencia manda, en la misma aplicación.

What you take away

  • La unicidad global de un Durable Object es la garantía que reemplaza locks distribuidos: un ID, una instancia, en todo el mundo.

  • Cada objeto trae su propia SQLite privada vía ctx.storage.sql, así que no necesitás una base de datos externa para estado por-recurso.

  • El Worker nunca toca el objeto directo: siempre pasa por un stub obtenido con getByName, y ese nombre es la clave de todo tu modelo de datos.

  • Elegir bien entre KV (lecturas rápidas, eventualmente consistente) y Durable Objects (consistencia fuerte, coordinación) evita que fuerces un producto a hacer lo que no fue pensado para hacer.

This article was written from the official documentation linked below. The code comes from there, not from a model's memory.

Where this came from