# 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
