Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Cloudflare platform skill covering Workers, D1, R2, KV, AI, Durable Objects, and security.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/durable-objects/patterns.md
1# Durable Objects Patterns23## When to Use Which Pattern45| Need | Pattern | ID Strategy |6|------|---------|-------------|7| Rate limit per user/IP | Rate Limiting | `idFromName(identifier)` |8| Mutual exclusion | Distributed Lock | `idFromName(resource)` |9| >1K req/s throughput | Sharding | `newUniqueId()` or hash |10| Real-time updates | WebSocket Collab | `idFromName(room)` |11| User sessions | Session Management | `idFromName(sessionId)` |12| Background cleanup | Alarm-based | Any |1314## RPC vs fetch()1516**RPC** (compat ≥2024-04-03): Type-safe, simpler, default for new projects17**fetch()**: Legacy compat, HTTP semantics, proxying1819```typescript20const count = await stub.increment(); // RPC21const count = await (await stub.fetch(req)).json(); // fetch()22```2324## Sharding (High Throughput)2526Single DO ~1K req/s max. Shard for higher throughput:2728```typescript29export default {30async fetch(req: Request, env: Env): Promise<Response> {31const userId = new URL(req.url).searchParams.get("user");32const hash = hashCode(userId) % 100; // 100 shards33const id = env.COUNTER.idFromName(`shard:${hash}`);34return env.COUNTER.get(id).fetch(req);35}36};3738function hashCode(str: string): number {39let hash = 0;40for (let i = 0; i < str.length; i++) hash = ((hash << 5) - hash) + str.charCodeAt(i);41return Math.abs(hash);42}43```4445**Decisions:**46- **Shard count**: 10-1000 typical (start with 100, measure, adjust)47- **Shard key**: User ID, IP, session - must distribute evenly (use hash)48- **Aggregation**: Coordinator DO or external system (D1, R2)4950## Rate Limiting5152```typescript53async checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {54const req = this.ctx.storage.sql.exec("SELECT COUNT(*) as count FROM requests WHERE key = ? AND timestamp > ?", key, Date.now() - windowMs).one();55if (req.count >= limit) return false;56this.ctx.storage.sql.exec("INSERT INTO requests (key, timestamp) VALUES (?, ?)", key, Date.now());57return true;58}59```6061## Distributed Lock6263```typescript64private held = false;65async acquire(timeoutMs = 5000): Promise<boolean> {66if (this.held) return false;67this.held = true;68await this.ctx.storage.setAlarm(Date.now() + timeoutMs);69return true;70}71async release() { this.held = false; await this.ctx.storage.deleteAlarm(); }72async alarm() { this.held = false; } // Auto-release on timeout73```7475## Hibernation-Aware Pattern7677Preserve state across hibernation:7879```typescript80async fetch(req: Request): Promise<Response> {81const [client, server] = Object.values(new WebSocketPair());82const userId = new URL(req.url).searchParams.get("user");83server.serializeAttachment({ userId }); // Survives hibernation84this.ctx.acceptWebSocket(server, ["room:lobby"]);85server.send(JSON.stringify({ type: "init", state: this.ctx.storage.kv.get("state") }));86return new Response(null, { status: 101, webSocket: client });87}8889async webSocketMessage(ws: WebSocket, msg: string) {90const { userId } = ws.deserializeAttachment(); // Retrieve after wake91const state = this.ctx.storage.kv.get("state") || {};92state[userId] = JSON.parse(msg);93this.ctx.storage.kv.put("state", state);94for (const c of this.ctx.getWebSockets("room:lobby")) c.send(msg);95}96```9798## Real-time Collaboration99100Broadcast updates to all connected clients:101102```typescript103async webSocketMessage(ws: WebSocket, msg: string) {104const data = JSON.parse(msg);105this.ctx.storage.kv.put("doc", data.content); // Persist106for (const c of this.ctx.getWebSockets()) if (c !== ws) c.send(msg); // Broadcast107}108```109110### WebSocket Reconnection111112**Client-side** (exponential backoff):113```typescript114class ResilientWS {115private delay = 1000;116connect(url: string) {117const ws = new WebSocket(url);118ws.onclose = () => setTimeout(() => {119this.connect(url);120this.delay = Math.min(this.delay * 2, 30000);121}, this.delay);122}123}124```125126**Server-side** (cleanup on close):127```typescript128async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {129const { userId } = ws.deserializeAttachment();130this.ctx.storage.sql.exec("UPDATE users SET online = false WHERE id = ?", userId);131for (const c of this.ctx.getWebSockets()) c.send(JSON.stringify({ type: "user_left", userId }));132}133```134135## Session Management136137```typescript138async createSession(userId: string, data: object): Promise<string> {139const id = crypto.randomUUID(), exp = Date.now() + 86400000;140this.ctx.storage.sql.exec("INSERT INTO sessions VALUES (?, ?, ?, ?)", id, userId, JSON.stringify(data), exp);141await this.ctx.storage.setAlarm(exp);142return id;143}144145async getSession(id: string): Promise<object | null> {146const row = this.ctx.storage.sql.exec("SELECT data FROM sessions WHERE id = ? AND expires_at > ?", id, Date.now()).one();147return row ? JSON.parse(row.data) : null;148}149150async alarm() { this.ctx.storage.sql.exec("DELETE FROM sessions WHERE expires_at <= ?", Date.now()); }151```152153## Multiple Events (Single Alarm)154155Queue pattern to schedule multiple events:156157```typescript158async scheduleEvent(id: string, runAt: number) {159await this.ctx.storage.put(`event:${id}`, { id, runAt });160const curr = await this.ctx.storage.getAlarm();161if (!curr || runAt < curr) await this.ctx.storage.setAlarm(runAt);162}163164async alarm() {165const events = await this.ctx.storage.list({ prefix: "event:" }), now = Date.now();166let next = null;167for (const [key, ev] of events) {168if (ev.runAt <= now) {169await this.processEvent(ev);170await this.ctx.storage.delete(key);171} else if (!next || ev.runAt < next) next = ev.runAt;172}173if (next) await this.ctx.storage.setAlarm(next);174}175```176177## Graceful Cleanup178179Use `ctx.waitUntil()` to complete work after response:180181```typescript182async myMethod() {183const response = { success: true };184this.ctx.waitUntil(this.ctx.storage.sql.exec("DELETE FROM old_data WHERE timestamp < ?", cutoff));185return response;186}187```188189## Best Practices190191- **Design**: Use `idFromName()` for coordination, `newUniqueId()` for sharding, minimize constructor work192- **Storage**: Prefer SQLite, batch with transactions, set alarms for cleanup, use PITR before risky ops193- **Performance**: ~1K req/s per DO max - shard for more, cache in memory, use alarms for deferred work194- **Reliability**: Handle 503 with retry+backoff, design for cold starts, test migrations with `--dry-run`195- **Security**: Validate inputs in Workers, rate limit DO creation, use jurisdiction for compliance196197## See Also198199- **[API](./api.md)** - ctx methods, WebSocket handlers200- **[Gotchas](./gotchas.md)** - Hibernation caveats, common errors201- **[DO Storage](../do-storage/README.md)** - Storage patterns and transactions202