mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# Cloudflare Durable Objects
|
||||
|
||||
Expert guidance for building stateful applications with Cloudflare Durable Objects.
|
||||
|
||||
## Reading Order
|
||||
|
||||
1. **First time?** Read this overview + Quick Start
|
||||
2. **Setting up?** See [Configuration](./configuration.md)
|
||||
3. **Building features?** Use decision trees below → [Patterns](./patterns.md)
|
||||
4. **Debugging issues?** Check [Gotchas](./gotchas.md)
|
||||
5. **Deep dive?** [API](./api.md) and [DO Storage](../do-storage/README.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Durable Objects combine compute with storage in globally-unique, strongly-consistent packages:
|
||||
- **Globally unique instances**: Each DO has unique ID for multi-client coordination
|
||||
- **Co-located storage**: Fast, strongly-consistent storage with compute
|
||||
- **Automatic placement**: Objects spawn near first request location
|
||||
- **Stateful serverless**: In-memory state + persistent storage
|
||||
- **Single-threaded**: Serial request processing (no race conditions)
|
||||
|
||||
## Rules of Durable Objects
|
||||
|
||||
Critical rules preventing most production issues:
|
||||
|
||||
1. **One alarm per DO** - Schedule multiple events via queue pattern
|
||||
2. **~1K req/s per DO max** - Shard for higher throughput
|
||||
3. **Constructor runs every wake** - Keep initialization light; use lazy loading
|
||||
4. **Hibernation clears memory** - In-memory state lost; persist critical data
|
||||
5. **Use `ctx.waitUntil()` for cleanup** - Ensures completion after response sent
|
||||
6. **No setTimeout for persistence** - Use `setAlarm()` for reliable scheduling
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Class Structure
|
||||
All DOs extend `DurableObject` base class with constructor receiving `DurableObjectState` (storage, WebSockets, alarms) and `Env` (bindings).
|
||||
|
||||
### Lifecycle States
|
||||
|
||||
```
|
||||
[Not Created] → [Active] ⇄ [Hibernated] → [Evicted]
|
||||
↓
|
||||
[Destroyed]
|
||||
```
|
||||
|
||||
- **Not Created**: DO ID exists but instance never spawned
|
||||
- **Active**: Processing requests, in-memory state valid, billed per GB-hour
|
||||
- **Hibernated**: WebSocket connections open but zero compute, zero cost
|
||||
- **Evicted**: Removed from memory; next request triggers cold start
|
||||
- **Destroyed**: Data deleted via migration or manual deletion
|
||||
|
||||
### Accessing from Workers
|
||||
Workers use bindings to get stubs, then call RPC methods directly (recommended) or use fetch handler (legacy).
|
||||
|
||||
**RPC vs fetch() decision:**
|
||||
```
|
||||
├─ New project + compat ≥2024-04-03 → RPC (type-safe, simpler)
|
||||
├─ Need HTTP semantics (headers, status) → fetch()
|
||||
├─ Proxying requests to DO → fetch()
|
||||
└─ Legacy compatibility → fetch()
|
||||
```
|
||||
|
||||
See [Patterns: RPC vs fetch()](./patterns.md) for examples.
|
||||
|
||||
### ID Generation
|
||||
- `idFromName()`: Deterministic, named coordination (rate limiting, locks)
|
||||
- `newUniqueId()`: Random IDs for sharding high-throughput workloads
|
||||
- `idFromString()`: Derive from existing IDs
|
||||
- Jurisdiction option: Data locality compliance
|
||||
|
||||
### Storage Options
|
||||
|
||||
**Which storage API?**
|
||||
```
|
||||
├─ Structured data, relations, transactions → SQLite (recommended)
|
||||
├─ Simple KV on SQLite DO → ctx.storage.kv (sync KV)
|
||||
└─ Legacy KV-only DO → ctx.storage (async KV)
|
||||
```
|
||||
|
||||
- **SQLite** (recommended): Structured data, transactions, 10GB/DO
|
||||
- **Synchronous KV API**: Simple key-value on SQLite objects
|
||||
- **Asynchronous KV API**: Legacy/advanced use cases
|
||||
|
||||
See [DO Storage](../do-storage/README.md) for deep dive.
|
||||
|
||||
### Special Features
|
||||
- **Alarms**: Schedule future execution per-DO (1 per DO - use queue pattern for multiple)
|
||||
- **WebSocket Hibernation**: Zero-cost idle connections (memory cleared on hibernation)
|
||||
- **Point-in-Time Recovery**: Restore to any point in 30 days (SQLite only)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
export class Counter extends DurableObject<Env> {
|
||||
async increment(): Promise<number> {
|
||||
const result = this.ctx.storage.sql.exec(
|
||||
`INSERT INTO counters (id, value) VALUES (1, 1)
|
||||
ON CONFLICT(id) DO UPDATE SET value = value + 1
|
||||
RETURNING value`
|
||||
).one();
|
||||
return result.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Worker access
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const id = env.COUNTER.idFromName("global");
|
||||
const stub = env.COUNTER.get(id);
|
||||
const count = await stub.increment();
|
||||
return new Response(`Count: ${count}`);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Decision Trees
|
||||
|
||||
### What do you need?
|
||||
|
||||
```
|
||||
├─ Coordinate requests (rate limit, lock, session)
|
||||
│ → idFromName(identifier) → [Patterns: Rate Limiting/Locks](./patterns.md)
|
||||
│
|
||||
├─ High throughput (>1K req/s)
|
||||
│ → Sharding with newUniqueId() or hash → [Patterns: Sharding](./patterns.md)
|
||||
│
|
||||
├─ Real-time updates (WebSocket, chat, collab)
|
||||
│ → WebSocket hibernation + room pattern → [Patterns: Real-time](./patterns.md)
|
||||
│
|
||||
├─ Background work (cleanup, notifications, scheduled tasks)
|
||||
│ → Alarms + queue pattern (1 alarm/DO) → [Patterns: Multiple Events](./patterns.md)
|
||||
│
|
||||
└─ User sessions with expiration
|
||||
→ Session pattern + alarm cleanup → [Patterns: Session Management](./patterns.md)
|
||||
```
|
||||
|
||||
### Which access pattern?
|
||||
|
||||
```
|
||||
├─ New project + typed methods → RPC (compat ≥2024-04-03)
|
||||
├─ Need HTTP semantics → fetch()
|
||||
├─ Proxying to DO → fetch()
|
||||
└─ Legacy compat → fetch()
|
||||
```
|
||||
|
||||
See [Patterns: RPC vs fetch()](./patterns.md) for examples.
|
||||
|
||||
### Which storage?
|
||||
|
||||
```
|
||||
├─ Structured data, SQL queries, transactions → SQLite (recommended)
|
||||
├─ Simple KV on SQLite DO → ctx.storage.kv (sync API)
|
||||
└─ Legacy KV-only DO → ctx.storage (async API)
|
||||
```
|
||||
|
||||
See [DO Storage](../do-storage/README.md) for complete guide.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
npx wrangler dev # Local dev with DOs
|
||||
npx wrangler dev --remote # Test against prod DOs
|
||||
npx wrangler deploy # Deploy + auto-apply migrations
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
**Docs**: https://developers.cloudflare.com/durable-objects/
|
||||
**API Reference**: https://developers.cloudflare.com/durable-objects/api/
|
||||
**Examples**: https://developers.cloudflare.com/durable-objects/examples/
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[Configuration](./configuration.md)** - wrangler.jsonc setup, migrations, bindings, environments
|
||||
- **[API](./api.md)** - Class structure, ctx methods, alarms, WebSocket hibernation
|
||||
- **[Patterns](./patterns.md)** - Sharding, rate limiting, locks, real-time, sessions
|
||||
- **[Gotchas](./gotchas.md)** - Limits, hibernation caveats, common errors
|
||||
|
||||
## See Also
|
||||
|
||||
- **[DO Storage](../do-storage/README.md)** - SQLite, KV, transactions (detailed storage guide)
|
||||
- **[Workers](../workers/README.md)** - Core Workers runtime features
|
||||
- **[WebSockets](../websockets/README.md)** - WebSocket APIs and patterns
|
||||
@@ -0,0 +1,187 @@
|
||||
# Durable Objects API
|
||||
|
||||
## Class Structure
|
||||
|
||||
```typescript
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
export class MyDO extends DurableObject<Env> {
|
||||
constructor(ctx: DurableObjectState, env: Env) {
|
||||
super(ctx, env);
|
||||
// Runs on EVERY wake - keep light!
|
||||
}
|
||||
|
||||
// RPC methods (called directly from worker)
|
||||
async myMethod(arg: string): Promise<string> { return arg; }
|
||||
|
||||
// fetch handler (legacy/HTTP semantics)
|
||||
async fetch(req: Request): Promise<Response> { /* ... */ }
|
||||
|
||||
// Lifecycle handlers
|
||||
async alarm() { /* alarm fired */ }
|
||||
async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) { /* ... */ }
|
||||
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { /* ... */ }
|
||||
async webSocketError(ws: WebSocket, error: unknown) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## DurableObjectState Context Methods
|
||||
|
||||
### Concurrency Control
|
||||
|
||||
```typescript
|
||||
// Complete work after response sent (e.g., cleanup, logging)
|
||||
this.ctx.waitUntil(promise: Promise<any>): void
|
||||
|
||||
// Critical section - blocks all other requests until complete
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
// No other requests processed during this block
|
||||
// Use for initialization or critical operations
|
||||
})
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- `waitUntil()`: Background cleanup, logging, non-critical work after response
|
||||
- `blockConcurrencyWhile()`: First-time init, schema migration, critical state setup
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```typescript
|
||||
this.ctx.id // DurableObjectId of this instance
|
||||
this.ctx.abort() // Force eviction (use after PITR restore to reload state)
|
||||
```
|
||||
|
||||
### Storage Access
|
||||
|
||||
```typescript
|
||||
this.ctx.storage.sql // SQLite API (recommended)
|
||||
this.ctx.storage.kv // Sync KV API (SQLite DOs only)
|
||||
this.ctx.storage // Async KV API (legacy/KV-only DOs)
|
||||
```
|
||||
|
||||
See **[DO Storage](../do-storage/README.md)** for complete storage API reference.
|
||||
|
||||
### WebSocket Management
|
||||
|
||||
```typescript
|
||||
this.ctx.acceptWebSocket(ws: WebSocket, tags?: string[]) // Enable hibernation
|
||||
this.ctx.getWebSockets(tag?: string): WebSocket[] // Get by tag or all
|
||||
this.ctx.getTags(ws: WebSocket): string[] // Get tags for connection
|
||||
```
|
||||
|
||||
### Alarms
|
||||
|
||||
```typescript
|
||||
await this.ctx.storage.setAlarm(timestamp: number | Date) // Schedule (overwrites existing)
|
||||
await this.ctx.storage.getAlarm(): number | null // Get next alarm time
|
||||
await this.ctx.storage.deleteAlarm(): void // Cancel alarm
|
||||
```
|
||||
|
||||
**Limit:** 1 alarm per DO. Use queue pattern for multiple events (see [Patterns](./patterns.md)).
|
||||
|
||||
## Storage APIs
|
||||
|
||||
For detailed storage documentation including SQLite queries, KV operations, transactions, and Point-in-Time Recovery, see **[DO Storage](../do-storage/README.md)**.
|
||||
|
||||
Quick reference:
|
||||
|
||||
```typescript
|
||||
// SQLite (recommended)
|
||||
this.ctx.storage.sql.exec("SELECT * FROM users WHERE id = ?", userId).one()
|
||||
|
||||
// Sync KV (SQLite DOs only)
|
||||
this.ctx.storage.kv.get("key")
|
||||
|
||||
// Async KV (legacy)
|
||||
await this.ctx.storage.get("key")
|
||||
```
|
||||
|
||||
## Alarms
|
||||
|
||||
Schedule future work that survives eviction:
|
||||
|
||||
```typescript
|
||||
// Set alarm (overwrites any existing alarm)
|
||||
await this.ctx.storage.setAlarm(Date.now() + 3600000) // 1 hour from now
|
||||
await this.ctx.storage.setAlarm(new Date("2026-02-01")) // Absolute time
|
||||
|
||||
// Check next alarm
|
||||
const nextRun = await this.ctx.storage.getAlarm() // null if none
|
||||
|
||||
// Cancel alarm
|
||||
await this.ctx.storage.deleteAlarm()
|
||||
|
||||
// Handler called when alarm fires
|
||||
async alarm() {
|
||||
// Runs once alarm triggers
|
||||
// DO wakes from hibernation if needed
|
||||
// Use for cleanup, notifications, scheduled tasks
|
||||
}
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- 1 alarm per DO maximum
|
||||
- Overwrites previous alarm when set
|
||||
- Use queue pattern for multiple scheduled events (see [Patterns](./patterns.md))
|
||||
|
||||
**Reliability:**
|
||||
- Alarms survive DO eviction/restart
|
||||
- Cloudflare retries failed alarms automatically
|
||||
- Not guaranteed exactly-once (handle idempotently)
|
||||
|
||||
## WebSocket Hibernation
|
||||
|
||||
Hibernation allows DOs with open WebSocket connections to consume zero compute/memory until message arrives.
|
||||
|
||||
```typescript
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const [client, server] = Object.values(new WebSocketPair());
|
||||
this.ctx.acceptWebSocket(server, ["room:123"]); // Tags for filtering
|
||||
server.serializeAttachment({ userId: "abc" }); // Persisted metadata
|
||||
return new Response(null, { status: 101, webSocket: client });
|
||||
}
|
||||
|
||||
// Called when message arrives (DO wakes from hibernation)
|
||||
async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
|
||||
const data = ws.deserializeAttachment(); // Retrieve metadata
|
||||
for (const c of this.ctx.getWebSockets("room:123")) c.send(msg);
|
||||
}
|
||||
|
||||
// Called on close (optional handler)
|
||||
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
|
||||
// Cleanup logic, remove from lists, etc.
|
||||
}
|
||||
|
||||
// Called on error (optional handler)
|
||||
async webSocketError(ws: WebSocket, error: unknown) {
|
||||
console.error("WebSocket error:", error);
|
||||
// Handle error, close connection, etc.
|
||||
}
|
||||
```
|
||||
|
||||
**Key concepts:**
|
||||
- **Auto-hibernation:** DO hibernates when no active requests/alarms
|
||||
- **Zero cost:** Hibernated DOs incur no charges while preserving connections
|
||||
- **Memory cleared:** All in-memory state lost on hibernation
|
||||
- **Attachment persistence:** Use `serializeAttachment()` for per-connection metadata that survives hibernation
|
||||
- **Tags for filtering:** Group connections by room/channel/user for targeted broadcasts
|
||||
|
||||
**Handler lifecycle:**
|
||||
- `webSocketMessage`: DO wakes, processes message, may hibernate after
|
||||
- `webSocketClose`: Called when client closes (optional - implement for cleanup)
|
||||
- `webSocketError`: Called on connection error (optional - implement for error handling)
|
||||
|
||||
**Metadata persistence:**
|
||||
```typescript
|
||||
// Store connection metadata (survives hibernation)
|
||||
ws.serializeAttachment({ userId: "abc", room: "lobby" })
|
||||
|
||||
// Retrieve after hibernation
|
||||
const { userId, room } = ws.deserializeAttachment()
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- **[DO Storage](../do-storage/README.md)** - Complete storage API reference
|
||||
- **[Patterns](./patterns.md)** - Real-world usage patterns
|
||||
- **[Gotchas](./gotchas.md)** - Hibernation caveats and limits
|
||||
@@ -0,0 +1,160 @@
|
||||
# Durable Objects Configuration
|
||||
|
||||
## Basic Setup
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01", // Use latest; ≥2024-04-03 for RPC
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "MY_DO", // Env binding name
|
||||
"class_name": "MyDO" // Class exported from this worker
|
||||
},
|
||||
{
|
||||
"name": "EXTERNAL", // Access DO from another worker
|
||||
"class_name": "ExternalDO",
|
||||
"script_name": "other-worker"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] } // Prefer SQLite
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Binding Options
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "BINDING_NAME",
|
||||
"class_name": "ClassName",
|
||||
"script_name": "other-worker", // Optional: external DO
|
||||
"environment": "production" // Optional: isolate by env
|
||||
}
|
||||
```
|
||||
|
||||
## Jurisdiction (Data Locality)
|
||||
|
||||
Specify jurisdiction at ID creation for data residency compliance:
|
||||
|
||||
```typescript
|
||||
// EU data residency
|
||||
const id = env.MY_DO.idFromName("user:123", { jurisdiction: "eu" })
|
||||
|
||||
// Available jurisdictions
|
||||
const jurisdictions = ["eu", "fedramp"] // More may be added
|
||||
|
||||
// All operations on this DO stay within jurisdiction
|
||||
const stub = env.MY_DO.get(id)
|
||||
await stub.someMethod() // Data stays in EU
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Set at ID creation time, immutable afterward
|
||||
- DO instance physically located within jurisdiction
|
||||
- Storage and compute guaranteed within boundary
|
||||
- Use for GDPR, FedRAMP, other compliance requirements
|
||||
- No cross-jurisdiction access (requests fail if DO in different jurisdiction)
|
||||
|
||||
## Migrations
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }, // Create SQLite (recommended)
|
||||
// { "tag": "v1", "new_classes": ["MyDO"] }, // Create KV (paid only)
|
||||
{ "tag": "v2", "renamed_classes": [{ "from": "Old", "to": "New" }] },
|
||||
{ "tag": "v3", "transferred_classes": [{ "from": "Src", "from_script": "old", "to": "Dest" }] },
|
||||
{ "tag": "v4", "deleted_classes": ["Obsolete"] } // Destroys ALL data!
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Migration rules:**
|
||||
- Tags must be unique and sequential (v1, v2, v3...)
|
||||
- No rollback supported (test with `--dry-run` first)
|
||||
- Auto-applied on deploy
|
||||
- `new_sqlite_classes` recommended over `new_classes` (SQLite vs KV)
|
||||
- `deleted_classes` immediately destroys ALL data (irreversible)
|
||||
|
||||
## Environment Isolation
|
||||
|
||||
Separate DO namespaces per environment (staging/production have distinct object instances):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"durable_objects": {
|
||||
"bindings": [{ "name": "MY_DO", "class_name": "MyDO" }]
|
||||
},
|
||||
"env": {
|
||||
"production": {
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{ "name": "MY_DO", "class_name": "MyDO", "environment": "production" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deploy: `npx wrangler deploy --env production`
|
||||
|
||||
## Limits & Settings
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"limits": {
|
||||
"cpu_ms": 300000 // Max CPU time: 30s default, 300s max
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [Gotchas](./gotchas.md) for complete limits table.
|
||||
|
||||
## Types
|
||||
|
||||
```typescript
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
|
||||
interface Env {
|
||||
MY_DO: DurableObjectNamespace<MyDO>;
|
||||
}
|
||||
|
||||
export class MyDO extends DurableObject<Env> {}
|
||||
|
||||
type DurableObjectNamespace<T> = {
|
||||
newUniqueId(options?: { jurisdiction?: string }): DurableObjectId;
|
||||
idFromName(name: string): DurableObjectId;
|
||||
idFromString(id: string): DurableObjectId;
|
||||
get(id: DurableObjectId): DurableObjectStub<T>;
|
||||
};
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npx wrangler dev # Local dev
|
||||
npx wrangler dev --remote # Test against production DOs
|
||||
|
||||
# Deployment
|
||||
npx wrangler deploy # Deploy + auto-apply migrations
|
||||
npx wrangler deploy --dry-run # Validate migrations without deploying
|
||||
npx wrangler deploy --env production
|
||||
|
||||
# Management
|
||||
npx wrangler durable-objects list # List namespaces
|
||||
npx wrangler durable-objects info <namespace> <id> # Inspect specific DO
|
||||
npx wrangler durable-objects delete <namespace> <id> # Delete DO (destroys data)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- **[API](./api.md)** - DurableObjectState and lifecycle handlers
|
||||
- **[Patterns](./patterns.md)** - Multi-environment patterns
|
||||
- **[Gotchas](./gotchas.md)** - Migration caveats, limits
|
||||
@@ -0,0 +1,197 @@
|
||||
# Durable Objects Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Hibernation Cleared My In-Memory State"
|
||||
|
||||
**Problem:** Variables lost after hibernation
|
||||
**Cause:** DO auto-hibernates when idle; in-memory state not persisted
|
||||
**Solution:** Use `ctx.storage` for critical data, `ws.serializeAttachment()` for per-connection metadata
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - lost on hibernation
|
||||
private userCount = 0;
|
||||
async webSocketMessage(ws: WebSocket, msg: string) {
|
||||
this.userCount++; // Lost!
|
||||
}
|
||||
|
||||
// ✅ Right - persisted
|
||||
async webSocketMessage(ws: WebSocket, msg: string) {
|
||||
const count = this.ctx.storage.kv.get("userCount") || 0;
|
||||
this.ctx.storage.kv.put("userCount", count + 1);
|
||||
}
|
||||
```
|
||||
|
||||
### "setTimeout Didn't Fire After Restart"
|
||||
|
||||
**Problem:** Scheduled work lost on eviction
|
||||
**Cause:** `setTimeout` in-memory only; eviction clears timers
|
||||
**Solution:** Use `ctx.storage.setAlarm()` for reliable scheduling
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - lost on eviction
|
||||
setTimeout(() => this.cleanup(), 3600000);
|
||||
|
||||
// ✅ Right - survives eviction
|
||||
await this.ctx.storage.setAlarm(Date.now() + 3600000);
|
||||
async alarm() { await this.cleanup(); }
|
||||
```
|
||||
|
||||
### "Constructor Runs on Every Wake"
|
||||
|
||||
**Problem:** Expensive init logic slows all requests
|
||||
**Cause:** Constructor runs on every wake (first request after eviction OR after hibernation)
|
||||
**Solution:** Lazy initialization or cache in storage
|
||||
|
||||
**Critical understanding:** Constructor runs in two scenarios:
|
||||
1. **Cold start** - DO evicted from memory, first request creates new instance
|
||||
2. **Wake from hibernation** - DO with WebSockets hibernated, message/alarm wakes it
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - expensive on every wake
|
||||
constructor(ctx: DurableObjectState, env: Env) {
|
||||
super(ctx, env);
|
||||
this.heavyData = this.loadExpensiveData(); // Slow!
|
||||
}
|
||||
|
||||
// ✅ Right - lazy load
|
||||
private heavyData?: HeavyData;
|
||||
private getHeavyData() {
|
||||
if (!this.heavyData) this.heavyData = this.loadExpensiveData();
|
||||
return this.heavyData;
|
||||
}
|
||||
```
|
||||
|
||||
### "Durable Object Overloaded (503 errors)"
|
||||
|
||||
**Problem:** 503 errors under load
|
||||
**Cause:** Single DO exceeding ~1K req/s throughput limit
|
||||
**Solution:** Shard across multiple DOs (see [Patterns: Sharding](./patterns.md))
|
||||
|
||||
### "Storage Quota Exceeded (Write failures)"
|
||||
|
||||
**Problem:** Write operations failing
|
||||
**Cause:** DO storage exceeding 10GB limit or account quota
|
||||
**Solution:** Cleanup with alarms, use `deleteAll()` for old data, upgrade plan
|
||||
|
||||
### "CPU Time Exceeded (Terminated)"
|
||||
|
||||
**Problem:** Request terminated mid-execution
|
||||
**Cause:** Processing exceeding 30s CPU time default limit
|
||||
**Solution:** Increase `limits.cpu_ms` in wrangler.jsonc (max 300s) or chunk work
|
||||
|
||||
### "WebSockets Disconnect on Eviction"
|
||||
|
||||
**Problem:** Connections drop unexpectedly
|
||||
**Cause:** DO evicted from memory without hibernation API
|
||||
**Solution:** Use WebSocket hibernation handlers + client reconnection logic
|
||||
|
||||
### "Migration Failed (Deploy error)"
|
||||
|
||||
**Cause:** Non-unique tags, non-sequential tags, or invalid class names in migration
|
||||
**Solution:** Check tag uniqueness/sequential ordering and verify class names are correct
|
||||
|
||||
### "RPC Method Not Found"
|
||||
|
||||
**Cause:** compatibility_date < 2024-04-03 preventing RPC usage
|
||||
**Solution:** Update compatibility_date to >= 2024-04-03 or use fetch() instead of RPC
|
||||
|
||||
### "Only One Alarm Allowed"
|
||||
|
||||
**Cause:** Need multiple scheduled tasks but only one alarm supported per DO
|
||||
**Solution:** Use event queue pattern to schedule multiple tasks with single alarm
|
||||
|
||||
### "Race Condition Despite Single-Threading"
|
||||
|
||||
**Problem:** Concurrent requests see inconsistent state
|
||||
**Cause:** Async operations allow request interleaving (await = yield point)
|
||||
**Solution:** Use `blockConcurrencyWhile()` for critical sections or atomic storage ops
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - race condition
|
||||
async incrementCounter() {
|
||||
const count = await this.ctx.storage.get("count") || 0;
|
||||
// ⚠️ Another request could execute here during await
|
||||
await this.ctx.storage.put("count", count + 1);
|
||||
}
|
||||
|
||||
// ✅ Right - atomic operation
|
||||
async incrementCounter() {
|
||||
return this.ctx.storage.sql.exec(
|
||||
"INSERT INTO counters (id, value) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET value = value + 1 RETURNING value"
|
||||
).one().value;
|
||||
}
|
||||
|
||||
// ✅ Right - explicit locking
|
||||
async criticalOperation() {
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
const count = await this.ctx.storage.get("count") || 0;
|
||||
await this.ctx.storage.put("count", count + 1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### "Migration Rollback Not Supported"
|
||||
|
||||
**Cause:** Attempting to rollback a migration after deployment
|
||||
**Solution:** Test with `--dry-run` before deploying; migrations cannot be rolled back
|
||||
|
||||
### "deleted_classes Destroys Data"
|
||||
|
||||
**Problem:** Migration deleted all data
|
||||
**Cause:** `deleted_classes` migration immediately destroys all DO instances and data
|
||||
**Solution:** Test with `--dry-run`; use `transferred_classes` to preserve data during moves
|
||||
|
||||
### "Cold Starts Are Slow"
|
||||
|
||||
**Problem:** First request after eviction takes longer
|
||||
**Cause:** DO constructor + initial storage access on cold start
|
||||
**Solution:** Expected behavior; optimize constructor, use connection pooling in clients, consider warming strategy for critical DOs
|
||||
|
||||
```typescript
|
||||
// Warming strategy (periodically ping critical DOs)
|
||||
export default {
|
||||
async scheduled(event: ScheduledEvent, env: Env) {
|
||||
const criticalIds = ["auth", "sessions", "locks"];
|
||||
await Promise.all(criticalIds.map(name => {
|
||||
const id = env.MY_DO.idFromName(name);
|
||||
const stub = env.MY_DO.get(id);
|
||||
return stub.ping(); // Keep warm
|
||||
}));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Free | Paid | Notes |
|
||||
|-------|------|------|-------|
|
||||
| SQLite storage per DO | 10 GB | 10 GB | Per Durable Object instance |
|
||||
| SQLite total storage | 5 GB | Unlimited | Account-wide quota |
|
||||
| Key+value size | 2 MB | 2 MB | Single KV pair (SQLite/async) |
|
||||
| CPU time default | 30s | 30s | Per request; configurable |
|
||||
| CPU time max | 300s | 300s | Set via `limits.cpu_ms` |
|
||||
| DO classes | 100 | 500 | Distinct DO class definitions |
|
||||
| SQL columns | 100 | 100 | Per table |
|
||||
| SQL statement size | 100 KB | 100 KB | Max SQL query size |
|
||||
| WebSocket message size | 32 MiB | 32 MiB | Per message |
|
||||
| Request throughput | ~1K req/s | ~1K req/s | Per DO (soft limit - shard for more) |
|
||||
| Alarms per DO | 1 | 1 | Use queue pattern for multiple events |
|
||||
| Total DOs | Unlimited | Unlimited | Create as many instances as needed |
|
||||
| WebSockets | Unlimited | Unlimited | Within 128MB memory limit per DO |
|
||||
| Memory per DO | 128 MB | 128 MB | In-memory state + WebSocket buffers |
|
||||
|
||||
## Hibernation Caveats
|
||||
|
||||
1. **Memory cleared** - All in-memory variables lost; reconstruct from storage or `deserializeAttachment()`
|
||||
2. **Constructor reruns** - Runs on wake; avoid expensive operations, use lazy initialization
|
||||
3. **No guarantees** - DO may evict instead of hibernate; design for both
|
||||
4. **Attachment limit** - `serializeAttachment()` data must be JSON-serializable, keep small
|
||||
5. **Alarm wakes DO** - Alarm prevents hibernation until handler completes
|
||||
6. **WebSocket state not automatic** - Must explicitly persist with `serializeAttachment()` or storage
|
||||
|
||||
## See Also
|
||||
|
||||
- **[Patterns](./patterns.md)** - Workarounds for common limitations
|
||||
- **[API](./api.md)** - Storage limits and quotas
|
||||
- **[Configuration](./configuration.md)** - Setting CPU limits
|
||||
@@ -0,0 +1,201 @@
|
||||
# Durable Objects Patterns
|
||||
|
||||
## When to Use Which Pattern
|
||||
|
||||
| Need | Pattern | ID Strategy |
|
||||
|------|---------|-------------|
|
||||
| Rate limit per user/IP | Rate Limiting | `idFromName(identifier)` |
|
||||
| Mutual exclusion | Distributed Lock | `idFromName(resource)` |
|
||||
| >1K req/s throughput | Sharding | `newUniqueId()` or hash |
|
||||
| Real-time updates | WebSocket Collab | `idFromName(room)` |
|
||||
| User sessions | Session Management | `idFromName(sessionId)` |
|
||||
| Background cleanup | Alarm-based | Any |
|
||||
|
||||
## RPC vs fetch()
|
||||
|
||||
**RPC** (compat ≥2024-04-03): Type-safe, simpler, default for new projects
|
||||
**fetch()**: Legacy compat, HTTP semantics, proxying
|
||||
|
||||
```typescript
|
||||
const count = await stub.increment(); // RPC
|
||||
const count = await (await stub.fetch(req)).json(); // fetch()
|
||||
```
|
||||
|
||||
## Sharding (High Throughput)
|
||||
|
||||
Single DO ~1K req/s max. Shard for higher throughput:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(req: Request, env: Env): Promise<Response> {
|
||||
const userId = new URL(req.url).searchParams.get("user");
|
||||
const hash = hashCode(userId) % 100; // 100 shards
|
||||
const id = env.COUNTER.idFromName(`shard:${hash}`);
|
||||
return env.COUNTER.get(id).fetch(req);
|
||||
}
|
||||
};
|
||||
|
||||
function hashCode(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||||
return Math.abs(hash);
|
||||
}
|
||||
```
|
||||
|
||||
**Decisions:**
|
||||
- **Shard count**: 10-1000 typical (start with 100, measure, adjust)
|
||||
- **Shard key**: User ID, IP, session - must distribute evenly (use hash)
|
||||
- **Aggregation**: Coordinator DO or external system (D1, R2)
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```typescript
|
||||
async checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {
|
||||
const req = this.ctx.storage.sql.exec("SELECT COUNT(*) as count FROM requests WHERE key = ? AND timestamp > ?", key, Date.now() - windowMs).one();
|
||||
if (req.count >= limit) return false;
|
||||
this.ctx.storage.sql.exec("INSERT INTO requests (key, timestamp) VALUES (?, ?)", key, Date.now());
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Distributed Lock
|
||||
|
||||
```typescript
|
||||
private held = false;
|
||||
async acquire(timeoutMs = 5000): Promise<boolean> {
|
||||
if (this.held) return false;
|
||||
this.held = true;
|
||||
await this.ctx.storage.setAlarm(Date.now() + timeoutMs);
|
||||
return true;
|
||||
}
|
||||
async release() { this.held = false; await this.ctx.storage.deleteAlarm(); }
|
||||
async alarm() { this.held = false; } // Auto-release on timeout
|
||||
```
|
||||
|
||||
## Hibernation-Aware Pattern
|
||||
|
||||
Preserve state across hibernation:
|
||||
|
||||
```typescript
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const [client, server] = Object.values(new WebSocketPair());
|
||||
const userId = new URL(req.url).searchParams.get("user");
|
||||
server.serializeAttachment({ userId }); // Survives hibernation
|
||||
this.ctx.acceptWebSocket(server, ["room:lobby"]);
|
||||
server.send(JSON.stringify({ type: "init", state: this.ctx.storage.kv.get("state") }));
|
||||
return new Response(null, { status: 101, webSocket: client });
|
||||
}
|
||||
|
||||
async webSocketMessage(ws: WebSocket, msg: string) {
|
||||
const { userId } = ws.deserializeAttachment(); // Retrieve after wake
|
||||
const state = this.ctx.storage.kv.get("state") || {};
|
||||
state[userId] = JSON.parse(msg);
|
||||
this.ctx.storage.kv.put("state", state);
|
||||
for (const c of this.ctx.getWebSockets("room:lobby")) c.send(msg);
|
||||
}
|
||||
```
|
||||
|
||||
## Real-time Collaboration
|
||||
|
||||
Broadcast updates to all connected clients:
|
||||
|
||||
```typescript
|
||||
async webSocketMessage(ws: WebSocket, msg: string) {
|
||||
const data = JSON.parse(msg);
|
||||
this.ctx.storage.kv.put("doc", data.content); // Persist
|
||||
for (const c of this.ctx.getWebSockets()) if (c !== ws) c.send(msg); // Broadcast
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket Reconnection
|
||||
|
||||
**Client-side** (exponential backoff):
|
||||
```typescript
|
||||
class ResilientWS {
|
||||
private delay = 1000;
|
||||
connect(url: string) {
|
||||
const ws = new WebSocket(url);
|
||||
ws.onclose = () => setTimeout(() => {
|
||||
this.connect(url);
|
||||
this.delay = Math.min(this.delay * 2, 30000);
|
||||
}, this.delay);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server-side** (cleanup on close):
|
||||
```typescript
|
||||
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
|
||||
const { userId } = ws.deserializeAttachment();
|
||||
this.ctx.storage.sql.exec("UPDATE users SET online = false WHERE id = ?", userId);
|
||||
for (const c of this.ctx.getWebSockets()) c.send(JSON.stringify({ type: "user_left", userId }));
|
||||
}
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
```typescript
|
||||
async createSession(userId: string, data: object): Promise<string> {
|
||||
const id = crypto.randomUUID(), exp = Date.now() + 86400000;
|
||||
this.ctx.storage.sql.exec("INSERT INTO sessions VALUES (?, ?, ?, ?)", id, userId, JSON.stringify(data), exp);
|
||||
await this.ctx.storage.setAlarm(exp);
|
||||
return id;
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<object | null> {
|
||||
const row = this.ctx.storage.sql.exec("SELECT data FROM sessions WHERE id = ? AND expires_at > ?", id, Date.now()).one();
|
||||
return row ? JSON.parse(row.data) : null;
|
||||
}
|
||||
|
||||
async alarm() { this.ctx.storage.sql.exec("DELETE FROM sessions WHERE expires_at <= ?", Date.now()); }
|
||||
```
|
||||
|
||||
## Multiple Events (Single Alarm)
|
||||
|
||||
Queue pattern to schedule multiple events:
|
||||
|
||||
```typescript
|
||||
async scheduleEvent(id: string, runAt: number) {
|
||||
await this.ctx.storage.put(`event:${id}`, { id, runAt });
|
||||
const curr = await this.ctx.storage.getAlarm();
|
||||
if (!curr || runAt < curr) await this.ctx.storage.setAlarm(runAt);
|
||||
}
|
||||
|
||||
async alarm() {
|
||||
const events = await this.ctx.storage.list({ prefix: "event:" }), now = Date.now();
|
||||
let next = null;
|
||||
for (const [key, ev] of events) {
|
||||
if (ev.runAt <= now) {
|
||||
await this.processEvent(ev);
|
||||
await this.ctx.storage.delete(key);
|
||||
} else if (!next || ev.runAt < next) next = ev.runAt;
|
||||
}
|
||||
if (next) await this.ctx.storage.setAlarm(next);
|
||||
}
|
||||
```
|
||||
|
||||
## Graceful Cleanup
|
||||
|
||||
Use `ctx.waitUntil()` to complete work after response:
|
||||
|
||||
```typescript
|
||||
async myMethod() {
|
||||
const response = { success: true };
|
||||
this.ctx.waitUntil(this.ctx.storage.sql.exec("DELETE FROM old_data WHERE timestamp < ?", cutoff));
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Design**: Use `idFromName()` for coordination, `newUniqueId()` for sharding, minimize constructor work
|
||||
- **Storage**: Prefer SQLite, batch with transactions, set alarms for cleanup, use PITR before risky ops
|
||||
- **Performance**: ~1K req/s per DO max - shard for more, cache in memory, use alarms for deferred work
|
||||
- **Reliability**: Handle 503 with retry+backoff, design for cold starts, test migrations with `--dry-run`
|
||||
- **Security**: Validate inputs in Workers, rate limit DO creation, use jurisdiction for compliance
|
||||
|
||||
## See Also
|
||||
|
||||
- **[API](./api.md)** - ctx methods, WebSocket handlers
|
||||
- **[Gotchas](./gotchas.md)** - Hibernation caveats, common errors
|
||||
- **[DO Storage](../do-storage/README.md)** - Storage patterns and transactions
|
||||
Reference in New Issue
Block a user