Agent skill
cloudflare-durable-objects
Build stateful Durable Objects for real-time apps, WebSocket servers, coordination, and persistent state. Prevents 20 documented errors. Use when: implementing chat rooms, multiplayer games, rate limiting, session management, WebSocket hibernation, or troubleshooting class export, migration, WebSocket state loss, boolean binding, RPC streams, or binding errors.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/cloudflare-durable-objects-dennislee928-smart-zone
SKILL.md
Cloudflare Durable Objects
Status: Production Ready ✅ Last Updated: 2026-01-21 Dependencies: cloudflare-worker-base (recommended) Latest Versions: wrangler@4.58.0, @cloudflare/workers-types@4.20260109.0 Official Docs: https://developers.cloudflare.com/durable-objects/
Recent Updates (2025):
- Oct 2025: WebSocket message size 1 MiB → 32 MiB, Data Studio UI for SQLite DOs (view/edit storage in dashboard)
- Aug 2025:
getByName()API shortcut for named DOs - June 2025: @cloudflare/actors library (beta) - recommended SDK with migrations, alarms, Actor class pattern. Note: Beta stability - see active issues before production use (RPC serialization, vitest integration, memory management)
- May 2025: Python Workers support for Durable Objects
- April 2025: SQLite GA with 10GB storage (beta → GA, 1GB → 10GB), Free tier access
- Feb 2025: PRAGMA optimize support, improved error diagnostics with reference IDs
Quick Start
Scaffold new DO project:
npm create cloudflare@latest my-durable-app -- --template=cloudflare/durable-objects-template --ts
Or add to existing Worker:
// src/counter.ts - Durable Object class
import { DurableObject } from 'cloudflare:workers';
export class Counter extends DurableObject {
async increment(): Promise<number> {
let value = (await this.ctx.storage.get<number>('value')) || 0;
await this.ctx.storage.put('value', ++value);
return value;
}
}
export default Counter; // CRITICAL: Export required
// wrangler.jsonc - Configuration
{
"durable_objects": {
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] } // SQLite backend (10GB limit)
]
}
// src/index.ts - Worker
import { Counter } from './counter';
export { Counter };
export default {
async fetch(request: Request, env: { COUNTER: DurableObjectNamespace<Counter> }) {
const stub = env.COUNTER.getByName('global-counter'); // Aug 2025: getByName() shortcut
return new Response(`Count: ${await stub.increment()}`);
}
};
DO Class Essentials
import { DurableObject } from 'cloudflare:workers';
export class MyDO extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // REQUIRED first line
// Load state before requests (optional)
ctx.blockConcurrencyWhile(async () => {
this.value = await ctx.storage.get('key') || defaultValue;
});
}
// RPC methods (recommended)
async myMethod(): Promise<string> { return 'Hello'; }
// HTTP fetch handler (optional)
async fetch(request: Request): Promise<Response> { return new Response('OK'); }
}
export default MyDO; // CRITICAL: Export required
// Worker must export DO class too
import { MyDO } from './my-do';
export { MyDO };
Constructor Rules:
- ✅ Call
super(ctx, env)first - ✅ Keep minimal - heavy work blocks hibernation wake
- ✅ Use
ctx.blockConcurrencyWhile()for storage initialization - ❌ Never
setTimeout/setInterval(use alarms) - ❌ Don't rely on in-memory state with WebSockets (persist to storage)
Storage API
Two backends available:
- SQLite (recommended): 10GB storage, SQL queries, atomic operations, PITR
- KV: 128MB storage, key-value only
Enable SQLite in migrations:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }] }
SQL API (SQLite backend)
export class MyDO extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY, text TEXT, created_at INTEGER);
CREATE INDEX IF NOT EXISTS idx_created ON messages(created_at);
PRAGMA optimize; // Feb 2025: Query performance optimization
`);
}
async addMessage(text: string): Promise<number> {
const cursor = this.sql.exec('INSERT INTO messages (text, created_at) VALUES (?, ?) RETURNING id', text, Date.now());
return cursor.one<{ id: number }>().id;
}
async getMessages(limit = 50): Promise<any[]> {
return this.sql.exec('SELECT * FROM messages ORDER BY created_at DESC LIMIT ?', limit).toArray();
}
}
SQL Methods:
sql.exec(query, ...params)→ cursorcursor.one<T>()→ single row (throws if none)cursor.one<T>({ allowNone: true })→ row or nullcursor.toArray<T>()→ all rowsctx.storage.transactionSync(() => { ... })→ atomic multi-statement
Best Practices:
- ✅ Use
?placeholders for parameterized queries - ✅ Create indexes on frequently queried columns
- ✅ Use
PRAGMA optimizeafter schema changes - ✅ Add
STRICTkeyword to table definitions to enforce type affinity and catch type mismatches early - ✅ Convert booleans to integers (0/1) - booleans bind as strings "true"/"false" in SQLite backend
Key-Value API (both backends)
// Single operations
await this.ctx.storage.put('key', value);
const value = await this.ctx.storage.get<T>('key');
await this.ctx.storage.delete('key');
// Batch operations
await this.ctx.storage.put({ key1: val1, key2: val2 });
const map = await this.ctx.storage.get(['key1', 'key2']);
await this.ctx.storage.delete(['key1', 'key2']);
// List and delete all
const map = await this.ctx.storage.list({ prefix: 'user:', limit: 100 });
await this.ctx.storage.deleteAll(); // Atomic on SQLite only
// Transactions
await this.ctx.storage.transaction(async (txn) => {
await txn.put('key1', val1);
await txn.put('key2', val2);
});
Storage Limits: SQLite 10GB (April 2025 GA) | KV 128MB
WebSocket Hibernation API
Capabilities:
- Thousands of WebSocket connections per instance
- Hibernate when idle (~10s no activity) to save costs
- Auto wake-up when messages arrive
- Message size limit: 32 MiB (Oct 2025, up from 1 MiB)
How it works:
- Active → handles messages
- Idle → ~10s no activity
- Hibernation → in-memory state cleared, WebSockets stay connected
- Wake → message arrives → constructor runs → handler called
CRITICAL: In-memory state is lost on hibernation. Use serializeAttachment() to persist per-WebSocket metadata.
Hibernation-Safe Pattern
export class ChatRoom extends DurableObject {
sessions: Map<WebSocket, { userId: string; username: string }>;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sessions = new Map();
// CRITICAL: Restore WebSocket metadata after hibernation
ctx.getWebSockets().forEach((ws) => {
this.sessions.set(ws, ws.deserializeAttachment());
});
}
async fetch(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
const url = new URL(request.url);
const metadata = { userId: url.searchParams.get('userId'), username: url.searchParams.get('username') };
// CRITICAL: Use ctx.acceptWebSocket(), NOT ws.accept()
this.ctx.acceptWebSocket(server);
server.serializeAttachment(metadata); // Persist across hibernation
this.sessions.set(server, metadata);
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
const session = this.sessions.get(ws);
// Handle message (max 32 MiB since Oct 2025)
}
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
this.sessions.delete(ws);
ws.close(code, 'Closing');
}
async webSocketError(ws: WebSocket, error: any): Promise<void> {
this.sessions.delete(ws);
}
}
Hibernation Rules:
- ✅
ctx.acceptWebSocket(ws)- enables hibernation - ✅
ws.serializeAttachment(data)- persist metadata - ✅
ctx.getWebSockets().forEach()- restore in constructor - ✅ Use alarms instead of
setTimeout/setInterval - ❌
ws.accept()- standard API, no hibernation - ❌
setTimeout/setInterval- prevents hibernation - ❌ In-progress
fetch()- blocks hibernation
Alarms API
Schedule DO to wake at future time. Use for: batching, cleanup, reminders, periodic tasks.
export class Batcher extends DurableObject {
async addItem(item: string): Promise<void> {
// Add to buffer
const buffer = await this.ctx.storage.get<string[]>('buffer') || [];
buffer.push(item);
await this.ctx.storage.put('buffer', buffer);
// Schedule alarm if not set
if ((await this.ctx.storage.getAlarm()) === null) {
await this.ctx.storage.setAlarm(Date.now() + 10000); // 10 seconds
}
}
async alarm(info: { retryCount: number; isRetry: boolean }): Promise<void> {
if (info.retryCount > 3) return; // Give up after 3 retries
const buffer = await this.ctx.storage.get<string[]>('buffer') || [];
await this.processBatch(buffer);
await this.ctx.storage.put('buffer', []);
// Alarm auto-deleted after success
}
}
API Methods:
await ctx.storage.setAlarm(Date.now() + 60000)- set alarm (overwrites existing)await ctx.storage.getAlarm()- get timestamp or nullawait ctx.storage.deleteAlarm()- cancel alarmasync alarm(info)- handler called when alarm fires
Behavior:
- ✅ At-least-once execution, auto-retries (up to 6x, exponential backoff)
- ✅ Survives hibernation/eviction
- ✅ Auto-deleted after success
- ⚠️ One alarm per DO (new alarm overwrites)
RPC vs HTTP Fetch
RPC (Recommended): Direct method calls, type-safe, simple
// DO class
export class Counter extends DurableObject {
async increment(): Promise<number> {
let value = (await this.ctx.storage.get<number>('count')) || 0;
await this.ctx.storage.put('count', ++value);
return value;
}
}
// Worker calls
const stub = env.COUNTER.getByName('my-counter');
const count = await stub.increment(); // Type-safe!
HTTP Fetch: Request/response pattern, required for WebSocket upgrades
// DO class
export class Counter extends DurableObject {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment') {
let value = (await this.ctx.storage.get<number>('count')) || 0;
await this.ctx.storage.put('count', ++value);
return new Response(JSON.stringify({ count: value }));
}
return new Response('Not found', { status: 404 });
}
}
// Worker calls
const stub = env.COUNTER.getByName('my-counter');
const response = await stub.fetch('https://fake-host/increment', { method: 'POST' });
const data = await response.json();
When to use: RPC for new projects (simpler), HTTP Fetch for WebSocket upgrades or complex routing
Getting DO Stubs
Three ways to get IDs:
idFromName(name)- Consistent routing (same name = same DO)
const stub = env.CHAT_ROOM.getByName('room-123'); // Aug 2025: Shortcut for idFromName + get
// Use for: chat rooms, user sessions, per-tenant logic, singletons
newUniqueId()- Random unique ID (must store for reuse)
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Optional: EU compliance
const idString = id.toString(); // Save to KV/D1 for later
idFromString(idString)- Recreate from saved ID
const id = env.MY_DO.idFromString(await env.KV.get('session:123'));
const stub = env.MY_DO.get(id);
Location hints (best-effort):
const stub = env.MY_DO.get(id, { locationHint: 'enam' }); // wnam, enam, sam, weur, eeur, apac, oc, afr, me
Jurisdiction (strict enforcement):
const id = env.MY_DO.newUniqueId({ jurisdiction: 'eu' }); // Options: 'eu', 'fedramp'
// Cannot combine with location hints, higher latency outside jurisdiction
Migrations
Required for: create, rename, delete, transfer DO classes
1. Create:
{ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }] } // SQLite 10GB
// Or: "new_classes": ["Counter"] // KV 128MB (legacy)
2. Rename:
{ "migrations": [
{ "tag": "v1", "new_sqlite_classes": ["OldName"] },
{ "tag": "v2", "renamed_classes": [{ "from": "OldName", "to": "NewName" }] }
]}
3. Delete:
{ "migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] },
{ "tag": "v2", "deleted_classes": ["Counter"] } // Immediate deletion, cannot undo
]}
4. Transfer:
{ "migrations": [{ "tag": "v1", "transferred_classes": [
{ "from": "OldClass", "from_script": "old-worker", "to": "NewClass" }
]}]}
Migration Rules:
- ❌ Atomic (all instances migrate at once, no gradual rollout)
- ❌ Tags are unique and append-only
- ❌ Cannot enable SQLite on existing KV-backed DOs
- ✅ Code changes don't need migrations (only schema changes)
- ✅ Class names globally unique per account
Common Patterns
Rate Limiting:
async checkLimit(userId: string, limit: number, window: number): Promise<boolean> {
const requests = (await this.ctx.storage.get<number[]>(`rate:${userId}`)) || [];
const valid = requests.filter(t => Date.now() - t < window);
if (valid.length >= limit) return false;
valid.push(Date.now());
await this.ctx.storage.put(`rate:${userId}`, valid);
return true;
}
Session Management with TTL:
async set(key: string, value: any, ttl?: number): Promise<void> {
const expiresAt = ttl ? Date.now() + ttl : null;
this.sql.exec('INSERT OR REPLACE INTO session (key, value, expires_at) VALUES (?, ?, ?)',
key, JSON.stringify(value), expiresAt);
}
async alarm(): Promise<void> {
this.sql.exec('DELETE FROM session WHERE expires_at < ?', Date.now());
await this.ctx.storage.setAlarm(Date.now() + 3600000); // Hourly cleanup
}
Leader Election:
async electLeader(workerId: string): Promise<boolean> {
try {
this.sql.exec('INSERT INTO leader (id, worker_id, elected_at) VALUES (1, ?, ?)', workerId, Date.now());
return true;
} catch { return false; } // Already has leader
}
Multi-DO Coordination:
// Coordinator delegates to child DOs
const gameRoom = env.GAME_ROOM.getByName(gameId);
await gameRoom.initialize();
await this.ctx.storage.put(`game:${gameId}`, { created: Date.now() });
Critical Rules
Always Do
✅ Export DO class from Worker
export class MyDO extends DurableObject { }
export default MyDO; // Required
✅ Call super(ctx, env) in constructor
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env); // Required first line
}
✅ Use new_sqlite_classes for new DOs
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
✅ Use ctx.acceptWebSocket() for hibernation
this.ctx.acceptWebSocket(server); // Enables hibernation
✅ Persist critical state to storage (not just memory)
await this.ctx.storage.put('important', value);
✅ Use alarms instead of setTimeout/setInterval
await this.ctx.storage.setAlarm(Date.now() + 60000);
✅ Use parameterized SQL queries
this.sql.exec('SELECT * FROM table WHERE id = ?', id);
✅ Minimize constructor work
constructor(ctx, env) {
super(ctx, env);
// Minimal initialization only
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}
Never Do
❌ Create DO without migration
// Missing migrations array = error
❌ Forget to export DO class
class MyDO extends DurableObject { }
// Missing: export default MyDO;
❌ Use setTimeout or setInterval
setTimeout(() => {}, 1000); // Prevents hibernation
❌ Rely only on in-memory state with WebSockets
// ❌ WRONG: this.sessions will be lost on hibernation
// ✅ CORRECT: Use serializeAttachment()
❌ Deploy migrations gradually
# Migrations are atomic - cannot use gradual rollout
❌ Enable SQLite on existing KV-backed DO
// Not supported - must create new DO class instead
❌ Use standard WebSocket API expecting hibernation
ws.accept(); // ❌ No hibernation
this.ctx.acceptWebSocket(ws); // ✅ Hibernation enabled
❌ Assume location hints are guaranteed
// Location hints are best-effort only
Known Issues Prevention
This skill prevents 20 documented issues:
Issue #1: Class Not Exported
Error: "binding not found" or "Class X not found"
Source: https://developers.cloudflare.com/durable-objects/get-started/
Why It Happens: DO class not exported from Worker
Prevention:
export class MyDO extends DurableObject { }
export default MyDO; // ← Required
Issue #2: Missing Migration
Error: "migrations required" or "no migration found for class"
Source: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
Why It Happens: Created DO class without migration entry
Prevention: Always add migration when creating new DO class
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }
]
}
Issue #3: Wrong Migration Type (KV vs SQLite)
Error: Schema errors, storage API mismatch
Source: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
Why It Happens: Used new_classes instead of new_sqlite_classes
Prevention: Use new_sqlite_classes for SQLite backend (recommended)
Issue #4: Constructor Overhead Blocks Hibernation Wake
Error: Slow hibernation wake-up times
Source: https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/
Why It Happens: Heavy work in constructor
Prevention: Minimize constructor, use blockConcurrencyWhile()
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
// Load from storage
});
}
Issue #5: setTimeout Breaks Hibernation
Error: DO never hibernates, high duration charges
Source: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
Why It Happens: setTimeout/setInterval prevents hibernation
Prevention: Use alarms API instead
// ❌ WRONG
setTimeout(() => {}, 1000);
// ✅ CORRECT
await this.ctx.storage.setAlarm(Date.now() + 1000);
Issue #6: In-Memory State Lost on Hibernation
Error: WebSocket metadata lost, state reset unexpectedly
Source: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
Why It Happens: Relied on in-memory state that's cleared on hibernation
Prevention: Use serializeAttachment() for WebSocket metadata
ws.serializeAttachment({ userId, username });
// Restore in constructor
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
Issue #7: Outgoing WebSocket Cannot Hibernate
Error: High charges despite hibernation API
Source: Cloudflare Docs | GitHub Issue #4864
Why It Happens: Durable Objects maintaining persistent connections to external WebSocket services using new WebSocket('url') cannot hibernate and remain pinned in memory indefinitely
Use Cases Affected:
- Real-time database subscriptions (Supabase, Firebase)
- Message brokers (Redis Streams, Apache Kafka)
- WebSocket connections to external real-time services
- Inter-service communication
Prevention: Only use hibernation for server-side (incoming) WebSockets via
ctx.acceptWebSocket(). Outgoing WebSocket connections created withnew WebSocket(url)prevent hibernation. Redesign architecture to avoid outgoing WebSocket connections from Durable Objects if hibernation is required.
Issue #8: Global Uniqueness Confusion
Error: Unexpected DO class name conflicts Source: https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness Why It Happens: DO class names are globally unique per account Prevention: Understand DO class names are shared across all Workers in account
Issue #9: deleteAll Issues
Error: Storage not fully deleted, billing continues; or internal error in alarm handler Source: KV Storage API | GitHub Issue #2993 Why It Happens:
- KV backend
deleteAll()can fail partially (not atomic) - SQLite: calling
deleteAll()in alarm handler causes internal error and retry loop (fixed in runtime) Prevention: - Use SQLite backend for atomic deleteAll
- In alarm handlers, call
deleteAlarm()BEFOREdeleteAll():
async alarm(info: { retryCount: number }): Promise<void> {
await this.ctx.storage.deleteAlarm(); // ← Call first
await this.ctx.storage.deleteAll(); // Then delete all
}
Issue #10: Binding Name Mismatch
Error: Runtime error accessing DO binding Source: https://developers.cloudflare.com/durable-objects/get-started/ Why It Happens: Binding name in wrangler.jsonc doesn't match code Prevention: Ensure consistency
{ "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] }
env.MY_DO.getByName('instance'); // Must match binding name
Issue #11: State Size Exceeded
Error: "state limit exceeded" or storage errors
Source: https://developers.cloudflare.com/durable-objects/platform/pricing/
Why It Happens: Exceeded 1GB (SQLite) or 128MB (KV) limit
Prevention: Monitor storage size, implement cleanup with alarms
Issue #12: Migration Not Atomic
Error: Gradual deployment blocked Source: https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/ Why It Happens: Tried to use gradual rollout with migrations Prevention: Migrations deploy atomically across all instances
Issue #13: Location Hint Ignored
Error: DO created in wrong region Source: https://developers.cloudflare.com/durable-objects/reference/data-location/ Why It Happens: Location hints are best-effort, not guaranteed Prevention: Use jurisdiction for strict requirements
Issue #14: Alarm Retry Failures
Error: Tasks lost after alarm failures Source: https://developers.cloudflare.com/durable-objects/api/alarms/ Why It Happens: Alarm handler throws errors repeatedly Prevention: Implement idempotent alarm handlers
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
return;
}
// Idempotent operation
}
Issue #15: Fetch Blocks Hibernation
Error: DO never hibernates despite using hibernation API
Source: https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/
Why It Happens: In-progress fetch() requests prevent hibernation
Prevention: Ensure all async I/O completes before idle period
Issue #16: Boolean Values Bind as Strings in SQLite
Error: Boolean columns contain strings "true"/"false" instead of integers 0/1; SQL queries with boolean comparisons fail
Source: GitHub Issue #9964
Why It Happens: JavaScript boolean values are serialized as strings in Durable Objects SQLite (inconsistent with D1 behavior)
Prevention: Manually convert booleans to integers and use STRICT tables
// Convert booleans to integers
this.sql.exec('INSERT INTO test (bool_col) VALUES (?)', value ? 1 : 0);
// Use STRICT tables to catch type mismatches early
this.sql.exec(`
CREATE TABLE IF NOT EXISTS test (
id INTEGER PRIMARY KEY,
bool_col INTEGER NOT NULL
) STRICT;
`);
Issue #17: RPC ReadableStream Cancel Logs False Network Errors
Error: Wrangler dev logs show "Network connection lost" when canceling ReadableStream from RPC, despite correct cancellation Source: GitHub Issue #11071 Why It Happens: Canceling ReadableStream returned from Durable Object via RPC triggers misleading error logs in Wrangler dev (presentation issue, not runtime bug) Prevention: No workaround available. The cancellation works correctly - ignore the false error logs in Wrangler dev. Issue does not appear in production or workerd-only setup.
Issue #18: blockConcurrencyWhile Does Not Block in Local Dev (Fixed)
Error: Constructor's blockConcurrencyWhile doesn't block requests in local dev, causing race conditions hidden during development
Source: GitHub Issue #8686
Why It Happens: Bug in older @cloudflare/vite-plugin and wrangler versions
Prevention: Upgrade to @cloudflare/vite-plugin v1.3.1+ and wrangler v4.18.0+ where this is fixed
Issue #19: RPC Between Multiple wrangler dev Sessions Not Supported
Error: "Cannot access MyDurableObject#myMethod as Durable Object RPC is not yet supported between multiple wrangler dev sessions"
Source: GitHub Issue #11944
Why It Happens: Accessing a Durable Object over RPC from multiple wrangler dev instances (e.g., separate Workers in monorepo) is not yet supported in local dev
Prevention: Use wrangler dev -c config1 -c config2 to run multiple workers in single session, or use HTTP fetch instead of RPC for cross-worker DO communication during local development
Issue #20: state.id.name Undefined in Constructor (vitest Regression)
Error: DurableObjectState.id.name is undefined in constructor when using @cloudflare/vitest-pool-workers 0.8.71
Source: GitHub Issue #11580
Why It Happens: Regression in vitest-pool-workers 0.8.71 (worked in 0.8.38)
Prevention: Downgrade to @cloudflare/vitest-pool-workers@0.8.38 or upgrade to later version where this is fixed
Configuration & Types
wrangler.jsonc:
{
"compatibility_date": "2025-11-23",
"durable_objects": {
"bindings": [{ "name": "COUNTER", "class_name": "Counter" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] },
{ "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterV2" }] }
]
}
TypeScript:
import { DurableObject, DurableObjectState, DurableObjectNamespace } from 'cloudflare:workers';
interface Env { MY_DO: DurableObjectNamespace<MyDurableObject>; }
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
}
}
Official Documentation
- Durable Objects: https://developers.cloudflare.com/durable-objects/
- State API (SQL): https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
- WebSocket Hibernation: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
- Alarms API: https://developers.cloudflare.com/durable-objects/api/alarms/
- Migrations: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
- Best Practices: https://developers.cloudflare.com/durable-objects/best-practices/
- Pricing: https://developers.cloudflare.com/durable-objects/platform/pricing/
Questions? Issues?
- Check
references/top-errors.mdfor common problems - Review
templates/for working examples - Consult official docs: https://developers.cloudflare.com/durable-objects/
- Verify migrations configuration carefully
Last verified: 2026-01-21 | Skill version: 3.1.0 | Changes: Added 5 new issues (boolean binding, RPC stream cancel, blockConcurrencyWhile local dev, RPC multi-session, vitest regression), expanded Issue #7 (outgoing WebSocket use cases) and Issue #9 (deleteAll alarm interaction), added STRICT tables best practice, updated @cloudflare/actors beta warning
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?