6.3 KiB
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()
// ✅ 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).
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 messagemessage(string | ArrayBuffer): The message data
webSocketClose(ws, code, reason, wasClean)
Called when WebSocket closes.
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 closedcode(number): Close codereason(string): Close reasonwasClean(boolean): True if closed cleanly
webSocketError(ws, error)
Called on WebSocket errors (not disconnections).
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
const metadata = { userId: '123', username: 'Alice' };
// Persist metadata
server.serializeAttachment(metadata);
// Track in-memory
this.sessions.set(server, metadata);
Deserialize in Constructor
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
// 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).
// 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.
// ✅ 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
// ❌ 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
// ❌ 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
// 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/setIntervalactive- In-progress
fetch()requests - Standard WebSocket API used (
ws.accept()instead ofctx.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/