Durable Objects -- Extending Real-Time Capabilities
Priority: P1 (High Value)
What are Durable Objects?
Workers with globally unique identity, persistent storage (SQLite or KV), and strong consistency. Each DO instance has:
- A unique name/ID (globally routable)
- Persistent state (survives restarts)
- Single-threaded execution (no race conditions)
- WebSocket support with hibernation
- Alarms for scheduled wake-ups
Current Durable Objects Usage
Company Manager \1 in 3 Workers:
1. Rate Limit Worker
- Per-user rate limiting with sliding windows
- Windows: 1s, 10s, 30s, 1m, 5m, 15m, 1h, 1d
- Batch rate limit checking
- `rate-limit-worker.wd29.workers.dev`
2. Gaming Worker
- Leaderboard management per game
- Real-time score updates via WebSocket
- User stats and rankings
- `gaming-worker.wd29.workers.dev`
3. Chat Worker
- Live chat rooms
- Message broadcasting
- `chat-worker.wd29.workers.dev`
4. Bookings Worker
- Availability checking
- Real-time booking updates
- `bookings-worker.wd29.workers.dev`
New Integration Opportunities
1. Multi-Tenant POS Session Management
\1: POS operations require real-time coordination -- multiple terminals at the same location need consistent state (open tabs, inventory holds, active discounts).
\1: One DO per POS location, managing all terminal sessions.
export class POSSession extends DurableObject {
private sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Initialize SQLite tables
this.sql.exec(`
CREATE TABLE IF NOT EXISTS open_tabs (
id TEXT PRIMARY KEY,
terminal_id TEXT NOT NULL,
items TEXT NOT NULL DEFAULT '[]',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS inventory_holds (
product_id TEXT NOT NULL,
quantity INTEGER NOT NULL,
terminal_id TEXT NOT NULL,
expires_at INTEGER NOT NULL
);
`);
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
case "/ws":
// WebSocket for real-time terminal updates
const pair = new WebSocketPair();
this.ctx.acceptWebSocket(pair[1]);
return new Response(null, { status: 101, webSocket: pair[0] });
case "/open-tab":
return this.openTab(await request.json());
case "/add-item":
return this.addItem(await request.json());
case "/hold-inventory":
return this.holdInventory(await request.json());
case "/close-tab":
return this.closeTab(await request.json());
default:
return new Response("Not Found", { status: 404 });
}
}
async webSocketMessage(ws: WebSocket, message: string) {
const data = JSON.parse(message);
// Handle real-time terminal coordination
// Broadcast state changes to all connected terminals
for (const client of this.ctx.getWebSockets()) {
if (client !== ws) {
client.send(JSON.stringify({ type: "state-update", data }));
}
}
}
// Alarm: clean up expired inventory holds
async alarm() {
this.sql.exec(
"DELETE FROM inventory_holds WHERE expires_at < ?",
Date.now()
);
// Schedule next cleanup in 5 minutes
this.ctx.storage.setAlarm(Date.now() + 5 * 60 * 1000);
}
}
\1: \1
2. Collaborative Document Editing
\1: The canvas feature needs real-time collaboration without conflicts.
\1: One DO per document, using operational transform or CRDT.
export class CollaborativeDocument extends DurableObject {
private sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec(`
CREATE TABLE IF NOT EXISTS operations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
op_type TEXT NOT NULL,
op_data TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS presence (
user_id TEXT PRIMARY KEY,
cursor_pos TEXT,
selection TEXT,
last_seen INTEGER
);
`);
}
async webSocketMessage(ws: WebSocket, message: string) {
const { type, userId, data } = JSON.parse(message);
switch (type) {
case "operation":
// Store operation
this.sql.exec(
"INSERT INTO operations (user_id, op_type, op_data, timestamp) VALUES (?, ?, ?, ?)",
userId, data.type, JSON.stringify(data), Date.now()
);
// Broadcast to other editors
this.broadcast(ws, { type: "remote-operation", userId, data });
break;
case "presence":
// Update cursor/selection
this.sql.exec(
"INSERT OR REPLACE INTO presence (user_id, cursor_pos, selection, last_seen) VALUES (?, ?, ?, ?)",
userId, JSON.stringify(data.cursor), JSON.stringify(data.selection), Date.now()
);
this.broadcast(ws, { type: "remote-presence", userId, data });
break;
}
}
private broadcast(sender: WebSocket, message: object) {
const json = JSON.stringify(message);
for (const ws of this.ctx.getWebSockets()) {
if (ws !== sender && ws.readyState === WebSocket.OPEN) {
ws.send(json);
}
}
}
}
\1: \1
3. Real-Time Notifications Hub
\1: No cross-instance notification delivery. Users on different server instances don't get real-time updates.
\1: One DO per user for notification delivery.
export class NotificationHub extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/ws") {
const pair = new WebSocketPair();
this.ctx.acceptWebSocket(pair[1]);
// Send any queued notifications
const queued = this.sql.exec("SELECT * FROM pending_notifications ORDER BY created_at").toArray();
for (const notif of queued) {
pair[1].send(JSON.stringify(notif));
}
// Clear delivered
this.sql.exec("DELETE FROM pending_notifications");
return new Response(null, { status: 101, webSocket: pair[0] });
}
if (url.pathname === "/notify" && request.method === "POST") {
const notification = await request.json();
const clients = this.ctx.getWebSockets();
if (clients.length > 0) {
// User is online -- deliver immediately
for (const ws of clients) {
ws.send(JSON.stringify(notification));
}
} else {
// User is offline -- queue for later
this.sql.exec(
"INSERT INTO pending_notifications (type, data, created_at) VALUES (?, ?, ?)",
notification.type, JSON.stringify(notification.data), Date.now()
);
}
return new Response("OK");
}
return new Response("Not Found", { status: 404 });
}
// WebSocket hibernation -- DO sleeps when no messages
async webSocketClose(ws: WebSocket) {
// User disconnected, future notifications will be queued
}
}
\1: \1
4. Workflow State Machine
\1: AI autonomy decisions need state coordination across distributed systems.
\1: One DO per active workflow/decision, tracking state transitions.
export class WorkflowState extends DurableObject {
async processDecision(decision: AIDecision): Promise<void> {
const current = this.sql.exec(
"SELECT * FROM workflow_state WHERE decision_id = ?",
decision.id
).one();
// State machine transitions
switch (current?.status) {
case "PENDING":
if (decision.action === "approve") {
await this.transitionTo(decision.id, "APPROVED");
await this.scheduleExecution(decision);
}
break;
case "APPROVED":
await this.transitionTo(decision.id, "EXECUTING");
// Execute via Queue
break;
case "EXECUTING":
await this.transitionTo(decision.id, "EXECUTED");
// Start rollback window timer
this.ctx.storage.setAlarm(Date.now() + 24 * 60 * 60 * 1000);
break;
}
}
async alarm() {
// Rollback window expired -- finalize
this.sql.exec(
"UPDATE workflow_state SET status = 'FINALIZED' WHERE status = 'EXECUTED'"
);
}
}
5. Floor Management / Table Ordering
\1: Restaurant floor management needs real-time table status and order coordination.
\1: One DO per restaurant floor.
export class FloorManager extends DurableObject {
// Tracks: table status, active orders, waiter assignments
// WebSocket: real-time floor view for all staff tablets
// Alarms: auto-close idle tables after timeout
}
\1: \1
6. Auction / Bidding System
For classified ads or e-commerce auctions:
export class AuctionRoom extends DurableObject {
// Strong consistency for bid ordering
// WebSocket for real-time bid updates
// Alarm for auction end time
// SQLite for bid history
}
Hibernatable WebSockets
Key optimization: DOs can hibernate (release memory) while maintaining WebSocket connections. The DO wakes only when a message arrives.
// Instead of this (always in memory):
this.websockets = new Set();
// Use this (hibernatable):
this.ctx.acceptWebSocket(ws);
this.ctx.getWebSockets(); // wakes DO only when needed
\1: You pay $0 for hibernated DOs. Only active compute is billed.
Multi-Tenant DO Naming Convention
{purpose}:{tenantId}:{entityId}
Examples:
pos-session:0195134f-8258-7e85:location-1
notifications:0195134f-8258-7e85:user-abc
floor:0195134f-8258-7e85:loc-1:floor-main
document:0195134f-8258-7e85:doc-xyz
auction:0195134f-8258-7e85:listing-123
Tenant isolation is guaranteed by the naming scheme -- DOs with different names are completely independent.
Limits
| Metric | Free | Paid |
|---|---|---|
| SQLite storage/DO | 5 GB total | 10 GB per DO |
| Requests/second/DO | 1,000 (soft) | 1,000 (soft) |
| CPU time/invocation | 30s (default) | Configurable |
| WebSocket msg size | 32 MiB | 32 MiB |
| Objects/namespace | Unlimited | Unlimited |
| Namespaces/account | Unlimited | Unlimited |
Pricing
| Metric | Included | Overage |
|---|---|---|
| Requests | 1M/month | $0.15/million |
| Duration | 400K GB-s | $12.50/M GB-s |
| SQLite storage | 5 GB | $0.20/GB-month |
| Hibernated | $0 | $0 |
Estimated Impact
- **Real-time capabilities**: Full collaboration, live notifications, POS coordination
- **Consistency**: Strong consistency for state coordination (no race conditions)
- **Scale**: Millions of concurrent DOs, each independently scalable
- **Cost**: Very low (hibernation = $0 for idle connections)
- **Effort**: 1-2 weeks per new DO class, extends existing Worker pattern