Workers es muy bueno respondiendo HTTP.
Una app real suele necesitar algo más:
- una sala de chat
- un carrito
- presencia en tiempo real
- rate limit por API key
- un agente de IA por conversación
- un documento colaborativo
En todos esos casos aparece la misma pregunta:
¿Quién recuerda el estado de esta entidad y coordina lo que pasa con ella?
Para eso existen Durable Objects.
Qué es un Durable Object
Un Durable Object es un tipo especial de Worker que combina compute + storage.
En palabras simples:
una entidad con identidad propia, código ejecutable y estado persistente.
Déjalo en inglés: Durable Object. Después de definirlo, puedes abreviarlo como DO. “Objeto durable” suena abstracto y no ayuda en una conversación de equipo.
Modelo mental: la key es la identidad
Piensa en entidades:
room:general
cart:user_123
counter:foo
agent:thread_456
Cada key apunta a una instancia específica.
- Misma key → mismo Durable Object
- Keys distintas → objetos distintos
counter:foo -> DO de foo
counter:bar -> DO de bar
Eso es lo importante: identidad.
No es solo una base de datos
Una base de datos guarda datos.
Un Durable Object puede guardar datos y ejecutar lógica para una entidad concreta.
Ejemplo: una sala de chat. No solo necesitas mensajes. También puedes necesitar:
- quién está conectado
- broadcast a sockets activos
- orden de eventos
- menos race conditions dentro de esa entidad
- limpieza de estado tras inactividad
Ese tipo de coordinación es incómodo si todo son funciones stateless sueltas y una tabla genérica.
Ejemplo: counter por key
Queremos:
/increment?name=foo
/increment?name=bar
foo y bar no comparten contador.
import { DurableObject } from "cloudflare:workers";
export interface Env {
COUNTERS: DurableObjectNamespace<Counter>;
}
export class Counter extends DurableObject<Env> {
async getValue(): Promise<number> {
return (await this.ctx.storage.get<number>("value")) ?? 0;
}
async increment(): Promise<number> {
const current = await this.getValue();
const next = current + 1;
await this.ctx.storage.put("value", next);
return next;
}
}
export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
const name = url.searchParams.get("name");
if (!name) {
return new Response("Falta ?name=foo", { status: 400 });
}
const id = env.COUNTERS.idFromName(name);
const counter = env.COUNTERS.get(id);
const value = await counter.increment();
return Response.json({ name, value });
},
} satisfies ExportedHandler<Env>;
La parte clave:
const id = env.COUNTERS.idFromName(name);
const counter = env.COUNTERS.get(id);
El name decide la identidad. El binding COUNTERS es la capacidad; la key es la instancia.
Flujo de un request
Para ?name=foo:
request
-> Worker
-> idFromName("foo")
-> Durable Object foo
-> lee estado
-> incrementa
-> guarda
-> responde
Otro request con foo vuelve al mismo DO. Uno con bar va a otro.
Casos donde encaja bien
| Caso | Key típica |
|---|---|
| Chat rooms | room:{roomId} |
| Carritos | cart:{sessionId} |
| Rate limit por entidad | limit:{apiKey} |
| Agentes AI | agent:{threadId} |
En cada uno, la unidad de coordinación es obvia: la sala, la sesión, la key, el hilo.
Error común: un DO global para todo
global:app
Si todos los requests pasan por una sola entidad global, creas un cuello de botella elegante.
Durable Objects funcionan mejor cuando separas por entidad:
room:{roomId}
cart:{sessionId}
tenant:{tenantId}
agent:{threadId}
La pregunta de diseño:
¿Cuál es la unidad natural de coordinación?
Cuándo NO usaría Durable Objects
No lo usaría solo porque “necesito guardar algo”.
| Necesitas | Mira primero |
|---|---|
| SQL | D1 |
| Object storage | R2 |
| Key-value simple | KV |
| Procesamiento async | Queues |
Usaría DO cuando necesitas las tres cosas juntas:
identidad + estado + coordinación
Checklist de diseño
Antes de crear un DO, responde:
- ¿Cuál es la entidad?
- ¿Cómo se construye la key?
- ¿Qué estado guarda?
- ¿Qué métodos expone?
- ¿Qué pasa si hay muchos objetos?
- ¿Qué pasa al despertar después de inactividad?
- ¿Qué logs necesitas por entidad?
- ¿Qué queda en el Worker y qué queda en el DO?
Ejemplo de diseño legible:
Entidad: sala de soporte
Key: support-room:{accountId}
Worker route: /rooms/:accountId/message
DO methods: addMessage, connectUser, getRecentMessages
Async: Queue para analytics
Ahí el sistema empieza a ser claro.
La frase para recordar
Worker = entrada HTTP
Durable Object = entidad con estado
key = identidad del objeto
Si tu problema suena a “necesito coordinar una sala, carrito, documento, tenant o agente”, Durable Objects merece estar en la conversación.
Serie Cloudflare para builders: Workers · Bindings · Durable Objects · Queues
Docs: Durable Objects · Examples · Bindings