Interactive Durable Objects pattern selection wizard. Helps choose the right DO pattern for your use case and generates implementation code with best practices.
Guides selection of optimal Durable Objects patterns and generates complete implementation code.
/plugin marketplace add secondsky/claude-skills/plugin install cloudflare-durable-objects@claude-skillsInteractive wizard to help select and implement the optimal Durable Objects pattern for your specific use case.
This command guides you through:
Use AskUserQuestion tool:
header: "Use Case" question: "What is your primary use case for Durable Objects?" multiSelect: false options:
header: "Scale" question: "What scale do you expect?" multiSelect: false options:
header: "Persistence" question: "What are your data persistence requirements?" multiSelect: false options:
header: "Queries" question: "What type of data queries do you need?" multiSelect: false options:
Based on answers, recommend appropriate pattern:
When: Real-time communication + Medium-Large scale Storage: Hybrid (connection state ephemeral, messages permanent) Queries: Basic filtering
Key Features:
Template: Load templates/websocket-hibernation-do.ts
Implementation:
import { DurableObject } from 'cloudflare:workers';
export class ChatRoom extends DurableObject {
private sessions: Map<WebSocket, { userId: string }> = new Map();
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Initialize SQL schema
this.ctx.blockConcurrencyWhile(async () => {
await this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
await this.ctx.storage.sql.exec(`
CREATE INDEX IF NOT EXISTS idx_created
ON messages(created_at DESC)
`);
});
}
async fetch(request: Request): Promise<Response> {
// Handle WebSocket upgrade
if (request.headers.get('Upgrade') === 'websocket') {
const pair = new WebSocketPair();
this.ctx.acceptWebSocket(pair[1]);
return new Response(null, { status: 101, webSocket: pair[0] });
}
// Handle HTTP requests (message history, etc.)
const url = new URL(request.url);
if (url.pathname === '/messages') {
const messages = await this.ctx.storage.sql.exec(
'SELECT * FROM messages ORDER BY created_at DESC LIMIT 50'
);
return Response.json(messages.rows);
}
return new Response('Not found', { status: 404 });
}
async webSocketMessage(ws: WebSocket, message: string) {
const data = JSON.parse(message);
const session = this.sessions.get(ws);
if (!session) return;
// Store message
await this.ctx.storage.sql.exec(
'INSERT INTO messages (user_id, content, created_at) VALUES (?, ?, ?)',
session.userId,
data.content,
Date.now()
);
// Broadcast to all connections
const broadcast = JSON.stringify({
userId: session.userId,
content: data.content,
timestamp: Date.now()
});
for (const [client] of this.sessions) {
client.send(broadcast);
}
}
async webSocketOpen(ws: WebSocket) {
const userId = crypto.randomUUID(); // Or from auth
this.sessions.set(ws, { userId });
}
async webSocketClose(ws: WebSocket) {
this.sessions.delete(ws);
}
}
wrangler.jsonc:
{
"durable_objects": {
"bindings": [
{
"name": "CHAT_ROOM",
"class_name": "ChatRoom"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["ChatRoom"]
}
]
}
When: Rate limiting + Any scale Storage: Session-based (TTL cleanup) Queries: Simple key-value
Key Features:
Implementation:
import { DurableObject } from 'cloudflare:workers';
export class RateLimiter extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.ctx.blockConcurrencyWhile(async () => {
await this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
endpoint TEXT NOT NULL
)
`);
await this.ctx.storage.sql.exec(`
CREATE INDEX IF NOT EXISTS idx_client_time
ON requests(client_id, timestamp DESC)
`);
// Schedule cleanup
await this.scheduleCleanup();
});
}
async checkLimit(clientId: string, limit: number, windowMs: number): Promise<boolean> {
const now = Date.now();
const windowStart = now - windowMs;
// Count recent requests
const result = await this.ctx.storage.sql.exec(
'SELECT COUNT(*) as count FROM requests WHERE client_id = ? AND timestamp > ?',
clientId,
windowStart
);
const count = result.rows[0].count as number;
if (count >= limit) {
return false; // Rate limit exceeded
}
// Record this request
await this.ctx.storage.sql.exec(
'INSERT INTO requests (client_id, timestamp, endpoint) VALUES (?, ?, ?)',
clientId,
now,
'api'
);
return true; // Request allowed
}
async alarm(): Promise<void> {
// Cleanup requests older than 1 hour
const cutoff = Date.now() - 3600_000;
const deleted = await this.ctx.storage.sql.exec(
'DELETE FROM requests WHERE timestamp < ?',
cutoff
);
console.log(`Cleaned up ${deleted.rowsWritten} old rate limit records`);
await this.scheduleCleanup();
}
async scheduleCleanup(): Promise<void> {
const nextCleanup = Date.now() + 3600_000; // 1 hour
await this.ctx.storage.setAlarm(nextCleanup);
}
}
Usage in Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const clientId = request.headers.get('CF-Connecting-IP') || 'unknown';
const id = env.RATE_LIMITER.idFromName(clientId);
const stub = env.RATE_LIMITER.get(id);
const allowed = await stub.checkLimit(clientId, 100, 60000); // 100 req/min
if (!allowed) {
return new Response('Rate limit exceeded', { status: 429 });
}
// Process request...
return new Response('Success');
}
};
When: Per-user state + Any scale Storage: Session-based (TTL cleanup) Queries: Basic filtering
Key Features:
Implementation:
import { DurableObject } from 'cloudflare:workers';
interface SessionData {
userId: string;
cart: Array<{ id: string; quantity: number }>;
preferences: Record<string, any>;
expiresAt: number;
}
export class UserSession extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.ctx.blockConcurrencyWhile(async () => {
await this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS session_data (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER NOT NULL
)
`);
await this.ctx.storage.sql.exec(`
CREATE INDEX IF NOT EXISTS idx_expires
ON session_data(expires_at)
WHERE expires_at IS NOT NULL
`);
await this.scheduleCleanup();
});
}
async getSession(): Promise<SessionData | null> {
const result = await this.ctx.storage.sql.exec(
'SELECT value FROM session_data WHERE key = ? AND expires_at > ?',
'session',
Date.now()
);
if (result.rows.length === 0) return null;
return JSON.parse(result.rows[0].value as string);
}
async updateSession(data: Partial<SessionData>): Promise<void> {
const current = await this.getSession();
const updated = { ...current, ...data };
const expiresAt = Date.now() + 86400_000; // 24 hours
await this.ctx.storage.sql.exec(
'INSERT OR REPLACE INTO session_data (key, value, expires_at) VALUES (?, ?, ?)',
'session',
JSON.stringify(updated),
expiresAt
);
}
async addToCart(itemId: string, quantity: number): Promise<void> {
const session = await this.getSession();
const cart = session?.cart || [];
const existingIndex = cart.findIndex(item => item.id === itemId);
if (existingIndex >= 0) {
cart[existingIndex].quantity += quantity;
} else {
cart.push({ id: itemId, quantity });
}
await this.updateSession({ cart });
}
async alarm(): Promise<void> {
const deleted = await this.ctx.storage.sql.exec(
'DELETE FROM session_data WHERE expires_at <= ?',
Date.now()
);
if (deleted.rowsWritten > 0) {
await this.scheduleCleanup();
}
}
async scheduleCleanup(): Promise<void> {
const nextCleanup = Date.now() + 3600_000;
await this.ctx.storage.setAlarm(nextCleanup);
}
}
When: Data aggregation + Any scale Storage: Permanent Queries: Basic filtering
Key Features:
Implementation:
import { DurableObject } from 'cloudflare:workers';
export class Analytics extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.ctx.blockConcurrencyWhile(async () => {
await this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
metric_name TEXT NOT NULL,
value REAL NOT NULL,
timestamp INTEGER NOT NULL
)
`);
await this.ctx.storage.sql.exec(`
CREATE INDEX IF NOT EXISTS idx_metric_time
ON metrics(metric_name, timestamp DESC)
`);
});
}
async increment(metricName: string, value: number = 1): Promise<void> {
await this.ctx.storage.sql.exec(
'INSERT INTO metrics (metric_name, value, timestamp) VALUES (?, ?, ?)',
metricName,
value,
Date.now()
);
}
async getTotal(metricName: string): Promise<number> {
const result = await this.ctx.storage.sql.exec(
'SELECT SUM(value) as total FROM metrics WHERE metric_name = ?',
metricName
);
return (result.rows[0]?.total as number) || 0;
}
async getTimeSeries(metricName: string, startTime: number, endTime: number): Promise<any[]> {
const result = await this.ctx.storage.sql.exec(`
SELECT
(timestamp / 60000) * 60000 as bucket,
SUM(value) as total
FROM metrics
WHERE metric_name = ?
AND timestamp BETWEEN ? AND ?
GROUP BY bucket
ORDER BY bucket
`, metricName, startTime, endTime);
return result.rows;
}
}
When: State coordination + Small-Medium scale Storage: Ephemeral Queries: Simple key-value
Key Features:
Implementation:
import { DurableObject } from 'cloudflare:workers';
export class LeaderElection extends DurableObject {
private leader: string | null = null;
private lastHeartbeat: number = 0;
private readonly HEARTBEAT_TIMEOUT = 5000; // 5 seconds
async electLeader(candidateId: string): Promise<boolean> {
const now = Date.now();
// Check if current leader is still alive
if (this.leader && (now - this.lastHeartbeat) < this.HEARTBEAT_TIMEOUT) {
return this.leader === candidateId;
}
// Elect new leader
this.leader = candidateId;
this.lastHeartbeat = now;
return true;
}
async heartbeat(leaderId: string): Promise<boolean> {
if (this.leader !== leaderId) {
return false;
}
this.lastHeartbeat = Date.now();
return true;
}
async getLeader(): Promise<string | null> {
const now = Date.now();
if (this.leader && (now - this.lastHeartbeat) >= this.HEARTBEAT_TIMEOUT) {
this.leader = null;
}
return this.leader;
}
async releaseLeadership(leaderId: string): Promise<void> {
if (this.leader === leaderId) {
this.leader = null;
}
}
}
Based on query complexity, recommend storage backend:
When:
Benefits:
Migration:
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["YourDOClass"]
}
]
}
When:
Migration:
{
"migrations": [
{
"tag": "v1",
"new_classes": ["YourDOClass"]
}
]
}
After pattern selection, generate:
Durable Object Class
wrangler.jsonc Configuration
Worker Entry Point
Tests
For each pattern, provide:
Provide clear deployment path:
Local Testing
npm test # Run Vitest tests
wrangler dev # Local development
Deployment
wrangler deploy # Deploy to production
Monitoring
wrangler tail # Live logs
Optimization
Combine WebSocket pattern with rate limiting for spam prevention.
Track user behavior while maintaining session state.
Implement temporary counters with automatic expiration.
For more advanced use cases, load:
references/rpc-metadata.md - RpcTarget pattern for metadata accessreferences/gradual-deployments.md - Traffic splitting strategiesreferences/performance-optimization.md - Advanced optimizationtemplates/ttl-cleanup-do.ts - TTL cleanup patternsIf pattern doesn't fit requirements:
/do-debug command for specific issuesreferences/best-practices.md for alternativesreferences/data-modeling.md for schema designdo-pattern-implementer agent for custom patternsPattern Selection Summary:
Choose pattern based on use case, then customize for specific requirements.