10 KiB
Top 15+ Documented Errors and Solutions
Complete reference for common Durable Objects errors and how to prevent them.
1. Class Not Exported
Error: "binding not found", "Class X not found"
Source: https://developers.cloudflare.com/durable-objects/get-started/
Why It Happens: Durable Object class not exported from Worker
Solution:
export class MyDO extends DurableObject { }
// CRITICAL: Export as default
export default MyDO;
// In Worker, also export for Wrangler
export { MyDO };
2. Missing Migration
Error: "migrations required", "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
Solution: Always add migration when creating new DO class
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDO"]
}
]
}
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
Solution: Use new_sqlite_classes for SQLite backend (recommended)
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyDO"] // ← SQLite (1GB, atomic)
}
]
}
4. Constructor Overhead Blocks Hibernation Wake
Error: Slow hibernation wake-up times, high latency
Source: https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/
Why It Happens: Heavy work in constructor delays all requests
Solution: Minimize constructor, use blockConcurrencyWhile()
constructor(ctx, env) {
super(ctx, env);
// Minimal initialization
this.sessions = new Map();
// Load from storage (blocks requests until complete)
ctx.blockConcurrencyWhile(async () => {
this.data = await ctx.storage.get('data');
});
}
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
Solution: Use alarms API instead
// ❌ WRONG: Prevents hibernation
setTimeout(() => this.doWork(), 60000);
// ✅ CORRECT: Allows hibernation
await this.ctx.storage.setAlarm(Date.now() + 60000);
async alarm() {
this.doWork();
}
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
Solution: Use serializeAttachment() for WebSocket metadata
// Persist metadata
ws.serializeAttachment({ userId, username });
// Restore in constructor
constructor(ctx, env) {
super(ctx, env);
this.sessions = new Map();
ctx.getWebSockets().forEach(ws => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
7. Outgoing WebSocket Cannot Hibernate
Error: High charges despite using hibernation API
Source: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
Why It Happens: Outgoing WebSockets don't support hibernation
Solution: Only use hibernation for server-side (incoming) WebSockets
Note: DO must be WebSocket server, not client.
8. Global Uniqueness Confusion
Error: Unexpected DO class name conflicts across Workers
Source: https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness
Why It Happens: DO class names are globally unique per account
Solution: Understand scope and use unique class names
// Worker A
export class CounterA extends DurableObject { }
// Worker B
export class CounterB extends DurableObject { }
// ❌ WRONG: Both use "Counter" → conflict
9. Partial deleteAll on KV Backend
Error: Storage not fully deleted, billing continues
Source: https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/
Why It Happens: KV backend deleteAll() can fail partially
Solution: Use SQLite backend for atomic deleteAll
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] } // Atomic operations
]
}
10. Binding Name Mismatch
Error: Runtime error accessing DO binding, undefined
Source: https://developers.cloudflare.com/durable-objects/get-started/
Why It Happens: Binding name in wrangler.jsonc doesn't match code
Solution: Ensure consistency
{
"durable_objects": {
"bindings": [
{ "name": "MY_DO", "class_name": "MyDO" }
]
}
}
// Must match binding name
env.MY_DO.getByName('instance');
11. State Size Exceeded
Error: "state limit exceeded", storage errors
Source: https://developers.cloudflare.com/durable-objects/platform/pricing/
Why It Happens: Exceeded 1GB (SQLite) or 128MB (KV) limit
Solution: Monitor storage size, implement cleanup
async checkStorageSize(): Promise<void> {
const size = await this.estimateSize();
if (size > 900_000_000) { // 900MB
await this.cleanup();
}
}
async alarm() {
// Periodic cleanup
const cutoff = Date.now() - (30 * 24 * 60 * 60 * 1000);
this.sql.exec('DELETE FROM messages WHERE created_at < ?', cutoff);
await this.ctx.storage.setAlarm(Date.now() + 86400000);
}
12. Migration Not Atomic
Error: Gradual deployment blocked, migration errors
Why It Happens: Tried to use gradual rollout with migrations
Solution: Understand migrations deploy atomically
- All DO instances migrate at once
- Cannot use gradual deployment with migrations
- Test thoroughly before deploying
13. Location Hint Ignored
Error: DO created in wrong region, higher latency
Source: https://developers.cloudflare.com/durable-objects/reference/data-location/
Why It Happens: Location hints are best-effort, not guaranteed
Solution: Use jurisdiction for strict requirements
// ⚠️ Best-effort (not guaranteed)
const stub = env.MY_DO.get(id, { locationHint: 'enam' });
// ✅ Strictly enforced
const euId = env.MY_DO.newUniqueId({ jurisdiction: 'eu' });
const stub = env.MY_DO.get(euId);
14. Alarm Retry Failures
Error: Tasks lost after repeated alarm failures
Source: https://developers.cloudflare.com/durable-objects/api/alarms/
Why It Happens: Alarm handler throws errors repeatedly, exhausts retries
Solution: Implement idempotent alarm handlers with retry limits
async alarm(info: { retryCount: number }): Promise<void> {
if (info.retryCount > 3) {
console.error('Giving up after 3 retries');
// Log failure, clean up state
await this.logFailure();
return;
}
// Idempotent operation (safe to retry)
await this.processWithIdempotency();
}
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
Solution: Ensure all async I/O completes before idle period
async webSocketMessage(ws: WebSocket, message: string): Promise<void> {
// ✅ GOOD: Await all I/O before returning
const response = await fetch('https://api.example.com/data');
const data = await response.json();
ws.send(JSON.stringify(data));
// Handler completes → can hibernate
// ❌ BAD: Background fetch prevents hibernation
this.ctx.waitUntil(
fetch('https://api.example.com/log').then(r => r.json())
);
}
16. Cannot Enable SQLite on Existing KV DO
Error: Migration fails, schema errors
Source: https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
Why It Happens: Attempted to migrate existing KV-backed DO to SQLite
Solution: Create new SQLite-backed DO class, migrate data manually
// ❌ WRONG: Cannot change existing DO backend
{
"migrations": [
{ "tag": "v1", "new_classes": ["Counter"] }, // KV backend
{ "tag": "v2", "renamed_classes": [{ "from": "Counter", "to": "CounterSQLite" }] }
// This doesn't change backend!
]
}
// ✅ CORRECT: Create new class
{
"migrations": [
{ "tag": "v1", "new_classes": ["Counter"] },
{ "tag": "v2", "new_sqlite_classes": ["CounterV2"] }
]
}
// Then migrate data from Counter to CounterV2
17. SQL Injection Vulnerability
Error: Security vulnerability, data breach
Source: https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
Why It Happens: String concatenation in SQL queries
Solution: Always use parameterized queries
// ❌ WRONG: SQL injection risk
this.sql.exec(`SELECT * FROM users WHERE email = '${userEmail}'`);
// ✅ CORRECT: Parameterized query
this.sql.exec('SELECT * FROM users WHERE email = ?', userEmail);
18. Standard WebSocket API Used
Error: High duration charges, no hibernation
Source: https://developers.cloudflare.com/durable-objects/best-practices/websockets/
Why It Happens: Used ws.accept() instead of ctx.acceptWebSocket()
Solution: Use hibernation API
// ❌ WRONG: Standard API, no hibernation
server.accept();
// ✅ CORRECT: Hibernation API
this.ctx.acceptWebSocket(server);
Quick Error Lookup
| Error Message | Issue # | Quick Fix |
|---|---|---|
| "binding not found" | #1 | Export DO class |
| "migrations required" | #2 | Add migration |
| Slow wake-up | #4 | Minimize constructor |
| High duration charges | #5, #15 | Use alarms, await I/O |
| State lost | #6 | serializeAttachment |
| "state limit exceeded" | #11 | Implement cleanup |
| "SQL injection" | #17 | Parameterized queries |
For more help: Check official docs and GitHub issues at https://github.com/cloudflare/workerd/issues