Files
gh-jezweb-claude-skills-ski…/references/websocket-hibernation.md
2025-11-30 08:24:13 +08:00

290 lines
6.3 KiB
Markdown

# WebSocket Hibernation API Deep Dive
Complete guide to WebSocket hibernation for cost savings.
---
## Why WebSocket Hibernation?
Traditional WebSocket connections keep the Durable Object **active in memory**, incurring duration charges even when idle.
**With Hibernation:**
- ✅ DO hibernates when idle (~10 seconds no activity)
- ✅ WebSocket clients **stay connected** to Cloudflare edge
- ✅ DO wakes up automatically when messages arrive
-**Massive cost savings** for long-lived connections
**Cost Example:**
- 1000 WebSocket connections for 1 hour
- Without hibernation: ~$0.50/hour (assuming 90% idle time)
- With hibernation: ~$0.05/hour
- **~90% savings**
---
## Hibernation Lifecycle
```
1. ACTIVE → DO in memory, handling messages
2. IDLE → No messages for ~10 seconds
3. HIBERNATE → In-memory state cleared, WebSockets stay connected
4. WAKE → New message → constructor runs → handler called
```
**CRITICAL:** In-memory state is **LOST** on hibernation!
---
## Enable Hibernation
### Use `ctx.acceptWebSocket()`
```typescript
// ✅ CORRECT: Enables hibernation
this.ctx.acceptWebSocket(server);
// ❌ WRONG: Standard API, NO hibernation
server.accept();
```
**Only works for server-side (incoming) WebSockets.**
---
## Handler Methods
### `webSocketMessage(ws, message)`
Called when WebSocket receives a message (even if hibernated).
```typescript
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
if (typeof message === 'string') {
const data = JSON.parse(message);
// Handle message
}
}
```
**Parameters:**
- `ws` (WebSocket): The WebSocket that received the message
- `message` (string | ArrayBuffer): The message data
### `webSocketClose(ws, code, reason, wasClean)`
Called when WebSocket closes.
```typescript
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
// Cleanup
this.sessions.delete(ws);
// Close the WebSocket
ws.close(code, 'Durable Object closing WebSocket');
}
```
**Parameters:**
- `ws` (WebSocket): The WebSocket that closed
- `code` (number): Close code
- `reason` (string): Close reason
- `wasClean` (boolean): True if closed cleanly
### `webSocketError(ws, error)`
Called on WebSocket errors (not disconnections).
```typescript
async webSocketError(ws: WebSocket, error: any): Promise<void> {
console.error('WebSocket error:', error);
this.sessions.delete(ws);
}
```
---
## Persist Metadata with Attachments
Use `serializeAttachment()` / `deserializeAttachment()` to persist per-WebSocket metadata across hibernation.
### Serialize on Accept
```typescript
const metadata = { userId: '123', username: 'Alice' };
// Persist metadata
server.serializeAttachment(metadata);
// Track in-memory
this.sessions.set(server, metadata);
```
### Deserialize in Constructor
```typescript
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Restore WebSocket connections after hibernation
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
// Restore metadata
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
```
**CRITICAL:** Metadata is **persisted to storage**, not just memory.
---
## Get Active WebSockets
```typescript
// Get all WebSockets accepted by this DO
const webSockets = this.ctx.getWebSockets();
console.log(`${webSockets.length} active connections`);
// Filter by tag (if tagged)
const taggedWs = this.ctx.getWebSockets('room:123');
```
---
## Tag WebSockets (Optional)
Tag WebSockets for grouping (e.g., by room, channel).
```typescript
// Accept with tag
this.ctx.acceptWebSocket(server, ['room:123']);
// Get by tag
const roomSockets = this.ctx.getWebSockets('room:123');
// Get all tags
const tags = ws.getTags();
```
---
## When Hibernation Does NOT Occur
Hibernation is **blocked** if:
`setTimeout` or `setInterval` callbacks are pending
❌ In-progress `fetch()` request (awaited I/O)
❌ Standard WebSocket API used (not hibernation API)
❌ Request/event still being processed
❌ Outgoing WebSocket (DO is client, not server)
---
## Best Practices
### Minimize Constructor Work
Heavy work in constructor **delays wake-up**.
```typescript
// ✅ GOOD: Minimal constructor
constructor(ctx, env) {
super(ctx, env);
this.sessions = new Map();
ctx.getWebSockets().forEach((ws) => {
const metadata = ws.deserializeAttachment();
this.sessions.set(ws, metadata);
});
}
// ❌ BAD: Heavy work delays wake-up
constructor(ctx, env) {
super(ctx, env);
// Don't do expensive I/O here
await this.loadLotsOfData();
}
```
### Use Alarms, Not setTimeout
```typescript
// ❌ WRONG: Prevents hibernation
setTimeout(() => {
this.doSomething();
}, 60000);
// ✅ CORRECT: Use alarms
await this.ctx.storage.setAlarm(Date.now() + 60000);
async alarm() {
this.doSomething();
}
```
### Persist Critical State
```typescript
// ❌ WRONG: Only in-memory (lost on hibernation)
this.userCount = 42;
// ✅ CORRECT: Persist to storage
await this.ctx.storage.put('userCount', 42);
// Or use serializeAttachment for per-WebSocket data
ws.serializeAttachment({ userId, username });
```
---
## Debugging Hibernation
### Check if DO is Hibernating
```typescript
// Log in constructor
constructor(ctx, env) {
super(ctx, env);
console.log('DO woke up! Active WebSockets:', ctx.getWebSockets().length);
}
// If you see this log frequently, DO is hibernating
```
### Common Issues
**Issue:** DO never hibernates (high duration charges)
**Possible Causes:**
- `setTimeout`/`setInterval` active
- In-progress `fetch()` requests
- Standard WebSocket API used (`ws.accept()` instead of `ctx.acceptWebSocket()`)
**Solution:** Check for blocking operations, use alarms instead.
---
## Limitations
⚠️ **Hibernation only for server-side WebSockets**
- DO must be WebSocket server (accept connections)
- Outgoing WebSockets (DO as client) **cannot hibernate**
⚠️ **In-memory state is lost**
- Restore state in constructor
- Use `serializeAttachment()` for per-WebSocket metadata
- Use storage for DO-wide state
⚠️ **No WebSocket Standard API** with hibernation
- Cannot use `addEventListener('message', ...)`
- Must use handler methods (`webSocketMessage`, etc.)
---
**Official Docs**: https://developers.cloudflare.com/durable-objects/best-practices/websockets/