Alarms and WebSockets
Use hibernating WebSockets
Keep real-time clients connected while an idle object sleeps and restore important connection information after wake-up.
8 minute lesson
Durable Objects can accept many WebSockets for one room, which makes them a natural home for chat, presence, and live collaboration. The naive version has a cost problem: an object holding open connections stays in memory, and you pay for that duration even when nobody sends anything for hours.
The Hibernation API solves it. Cloudflare keeps the connections open while the JavaScript instance leaves memory. When a message arrives, the runtime recreates the instance and delivers it. Clients notice nothing; your bill notices a lot.
Accept sockets through the context, not with event listeners:
async fetch(request) {
const pair = new WebSocketPair()
const [client, server] = Object.values(pair)
this.ctx.acceptWebSocket(server)
server.serializeAttachment({ userId: request.headers.get('x-user-id') })
return new Response(null, { status: 101, webSocket: client })
}
async webSocketMessage(ws, message) {
const { userId } = ws.deserializeAttachment()
this.ctx.storage.sql.exec(
'insert into messages (author, body, created_at) values (?, ?, ?)',
userId, message, Date.now())
for (const socket of this.ctx.getWebSockets()) {
socket.send(JSON.stringify({ from: userId, body: message }))
}
}
acceptWebSocket plus the webSocketMessage, webSocketClose, and webSocketError handler methods are what make hibernation possible. The classic mistake is the older listener pattern:
// looks equivalent, quietly disables hibernation
server.accept()
server.addEventListener('message', (event) => { /* ... */ })
It works, but the object can never hibernate while such a connection is open, so it sits in memory around the clock. Silent, and expensive.
Design for amnesia
On wake-up, class properties are gone. Any this.connectedUsers map you built is empty, while this.ctx.getWebSockets() still returns every live socket. So split state by lifetime: store durable room data in SQLite, and per-connection details in WebSocket attachments. serializeAttachment stores a small serializable value with the socket itself, and it survives hibernation — that’s where the user ID above lives, which is why webSocketMessage can identify the sender with no memory of accepting the connection.
One performance note: every message to a hibernated object pays a wake-up. Batch very frequent small messages — cursor positions, typing indicators — on the client before sending, so runtime transitions do not become the bottleneck for chatty rooms.
Prove the design survives sleep. Connect two clients, hibernate the local object if the test tools allow it, then prove both receive the next persisted message. If the broadcast reaches both sockets after the instance died and returned, your state is in the right places.
Lesson completed