
Serverless promised us freedom. No servers to manage. No scaling to worry about. Just write your function and deploy.
And for simple CRUD operations, it delivered.
But the moment you need state—the moment you need to coordinate concurrent users editing the same document, manage a WebSocket connection that lives for hours, or schedule a task specific to a single user—serverless falls apart.
You end up spinning up a Redis cluster, a WebSocket server, or a full VM just to hold a little state. And just like that, you're back to managing infrastructure.
Cloudflare Durable Objects fix this. And once you understand them, you'll wonder how you ever built real-time applications without them.
The Problem: Stateless by Design
Traditional serverless functions (Lambda, Workers, Cloud Functions) are stateless by design. Each invocation starts from scratch. No memory of what happened before.
If 100 requests come in for "workspace-123", they might be handled by 100 different function instances across 100 different data centers. Each one is isolated and ephemeral.
This is great for scaling, terrible for coordination.
Want two users to edit the same document in real-time? You need a centralized coordination point. Traditionally, that meant:
- A Redis Pub/Sub cluster
- A dedicated WebSocket server
- A database with optimistic locking
All of which defeat the purpose of going serverless in the first place.
What Durable Objects Actually Are
A Durable Object is a globally unique, single-threaded, stateful instance that lives on Cloudflare's edge network.
The key insight: One ID = One Instance. Globally.
When you create a Durable Object with an ID like workspace-123, Cloudflare guarantees that only one instance of that object exists anywhere in the world. Every request targeting that ID is routed to the same instance, no matter where the request originates.
It's like having a dedicated micro-server per user, per room, or per document—except you only pay when it's active, and it scales to zero when idle.
// In your Worker (the entry point)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Get the unique ID for this workspace
const id = env.WORKSPACE.idFromName("workspace-123");
// Get a reference (stub) to that specific instance
const stub = env.WORKSPACE.get(id);
// Forward the request — this ALWAYS goes to the same instance
return stub.fetch(request);
}
};
// The Durable Object itself
export class Workspace {
private state: DurableObjectState;
private connections: Set<WebSocket> = new Set();
constructor(state: DurableObjectState, env: Env) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/ws") {
// Handle WebSocket upgrade
const pair = new WebSocketPair();
this.connections.add(pair[1]);
pair[1].accept();
pair[1].addEventListener("message", (msg) => {
// Broadcast to all connected users
for (const conn of this.connections) {
if (conn !== pair[1]) {
conn.send(msg.data as string);
}
}
});
return new Response(null, { status: 101, webSocket: pair[0] });
}
return new Response("Hello from Workspace!");
}
}
That's a real-time collaborative workspace in ~40 lines. No Redis. No WebSocket server. No infrastructure.
Four Storage Layers
One of the most powerful aspects of Durable Objects is the storage architecture. Each layer serves a different purpose:
| Storage Layer | Analogy | Speed | Persistence | Best For |
|---|---|---|---|---|
| In-Memory (class properties) | Your desk | ⚡ Instant | ❌ Lost on hibernation | Active WebSocket connections, session cache |
KV Storage (state.storage) | Desk drawer | 🔥 Fast | ✅ Yes | User config, metadata, simple state |
SQLite (state.storage.sql) | Filing cabinet | 📊 Fast + Queryable | ✅ Yes | Structured data, relationships, indexes |
| External (R2, D1) | Warehouse | 🏗️ Network call | ✅ Yes | Large files, cross-object analytics |
export class UserSession {
// In-memory: fast but lost on hibernation
private activeConnections: Map<string, WebSocket> = new Map();
async fetch(request: Request): Promise<Response> {
// KV Storage: simple, persistent key-value pairs
await this.state.storage.put("lastActive", Date.now());
const preferences = await this.state.storage.get("preferences");
// SQLite: structured, queryable data
this.state.storage.sql.exec(
`INSERT INTO activity_log (action, timestamp) VALUES (?, ?)`,
"page_view", Date.now()
);
// Query with SQL
const recentActivity = this.state.storage.sql.exec(
`SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT 10`
).toArray();
return Response.json({ preferences, recentActivity });
}
}
Alarms: Built-In Scheduling Per Object
Need to send a reminder 30 days after signup? Or auto-expire an inactive session?
Durable Objects have alarms—scheduled callbacks that execute on a specific object at a specific time.
export class Subscription {
async createTrial(userId: string): Promise<Response> {
await this.state.storage.put("userId", userId);
await this.state.storage.put("plan", "trial");
// Set alarm for 14 days from now
const trialEnd = Date.now() + 14 * 24 * 60 * 60 * 1000;
await this.state.storage.setAlarm(trialEnd);
return Response.json({ message: "Trial started", expiresAt: trialEnd });
}
// This runs automatically when the alarm fires
async alarm(): Promise<void> {
const userId = await this.state.storage.get("userId");
const plan = await this.state.storage.get("plan");
if (plan === "trial") {
// Trial expired — send notification, downgrade, etc.
await notifyUser(userId, "Your trial has expired!");
await this.state.storage.put("plan", "expired");
}
}
}
No cron jobs. No separate scheduler service. The alarm lives with the object it belongs to.
When to Use Durable Objects
Perfect for:
- 🎯 Per-user or per-tenant state (SaaS multitenant apps)
- 🤖 AI Agents (each agent is its own Durable Object with memory)
- 📝 Real-time collaboration (Google Docs-style)
- 💬 Chat rooms and WebSocket servers
- ⏰ Per-entity scheduled tasks (subscription renewals, reminders)
- 🚦 Rate limiting per user
- 🔒 Distributed locks and coordination
Not ideal for:
- 📦 Storing large files (use R2)
- 📊 Cross-user analytics (use Workers Analytics Engine or D1)
- ⚡ Truly stateless operations (use regular Workers)
- 🔍 Queries across all users (use D1 with proper indexes)
The AI Agent Connection
Here's where Durable Objects get really interesting in 2026: AI Agents.
Each AI agent needs:
- Persistent memory across conversations
- The ability to run background tasks
- WebSocket connections for real-time streaming
- Scheduled actions (follow-ups, reminders)
A Durable Object is literally the perfect primitive for this. One Durable Object per agent. Each agent has its own memory, its own WebSocket connections, and its own scheduled alarms.
Cloudflare actually built their Agents SDK on top of Durable Objects for exactly this reason.
Pitfalls to Watch For
After working with Durable Objects, here are the gotchas:
1. Single-Threaded Per Instance
Each Durable Object processes one request at a time. If you have a viral chat room with 10,000 concurrent users, that single thread becomes a bottleneck. Solution: Shard across multiple objects.
2. Cold Starts Are Real
If a Durable Object has been hibernating, the first request takes longer (it needs to re-instantiate). Design your UX accordingly.
3. Don't Treat It Like a Database
Durable Objects are for coordination and state, not for storing terabytes of data. Use R2 and D1 for heavy storage.
The Bottom Line
Durable Objects solve the fundamental tension of serverless: how to be stateless at the infrastructure level while being stateful at the application level.
They give you:
- The simplicity of serverless (no servers to manage)
- The power of stateful servers (memory, WebSockets, coordination)
- The efficiency of edge computing (runs close to your users)
- The economics of pay-per-use (scales to zero)
If you're building real-time apps, AI agents, or multi-tenant SaaS—Durable Objects aren't optional anymore. They're the foundation.
Are you building with Durable Objects? I'd love to hear what you're using them for. Drop a comment below!