Alarms and WebSockets

Use hibernating WebSockets

Keep real-time clients connected while an idle object sleeps and restore important connection information after wake-up.

A Durable Object can hold many WebSockets for one room. That makes it 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 time even when nobody sends anything for hours.

The Hibernation API fixes 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 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. Yet this.ctx.getWebSockets() still returns every live socket.

So split state by lifetime. Durable room data goes in SQLite. Per-connection details go in the WebSocket attachment. serializeAttachment stores a small serializable value with the socket itself, and it survives hibernation. That’s where the user ID above lives. It’s 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. For very frequent small messages, like cursor positions or typing indicators, batch them on the client before sending. Otherwise the wake-ups become the bottleneck for chatty rooms.

Try this to prove the design survives sleep. Connect two clients, hibernate the local object if your test tools allow it, then send one message. If both sockets receive the broadcast after the instance died and came back, your state is in the right places.

Lesson completed