| name | cloudflare-do |
| description | Cloudflare Durable Objects patterns for agent state. Use when implementing agent DOs, WebSocket handling, hibernation, storage API, alarms, or DO-to-DO communication. Triggers on Durable Object, DO state, WebSocket server, hibernation, agent persistence. |
Cloudflare Durable Objects
Durable Objects provide strongly consistent, single-threaded state for each agent.
Core Pattern: Agent as Durable Object
import { DurableObject } from 'cloudflare:workers'
export class AgentDO extends DurableObject {
private initialized = false
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env)
}
async fetch(request: Request): Promise<Response> {
if (!this.initialized) {
await this.initialize()
}
if (request.headers.get('Upgrade') === 'websocket') {
return this.handleWebSocket(request)
}
const url = new URL(request.url)
switch (url.pathname) {
case '/prompt':
return this.handlePrompt(request)
case '/memory':
return this.handleMemory(request)
default:
return new Response('Not found', { status: 404 })
}
}
private async initialize() {
this.initialized = true
}
}
WebSocket with Hibernation
Hibernatable WebSockets allow DO to sleep while connections stay open:
export class AgentDO extends DurableObject {
async handleWebSocket(request: Request): Promise<Response> {
const pair = new WebSocketPair()
const [client, server] = Object.values(pair)
server.serializeAttachment({
connectedAt: Date.now(),
subscriptions: ['agent.memory.*']
})
this.ctx.acceptWebSocket(server)
return new Response(null, { status: 101, webSocket: client })
}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
const attachment = ws.deserializeAttachment() as ConnectionMeta
const data = JSON.parse(message as string)
const response = await this.processMessage(data)
ws.send(JSON.stringify(response))
}
async webSocketClose(ws: WebSocket, code: number, reason: string) {
}
async webSocketError(ws: WebSocket, error: unknown) {
console.error('WebSocket error:', error)
}
}
Storage API
Key-value storage with strong consistency:
await this.ctx.storage.put('key', value)
await this.ctx.storage.put({ key1: val1, key2: val2 })
const val = await this.ctx.storage.get('key')
const vals = await this.ctx.storage.get(['key1', 'key2'])
const entries = await this.ctx.storage.list({ prefix: 'memory:' })
await this.ctx.storage.delete('key')
await this.ctx.storage.deleteAll()
await this.ctx.storage.transaction(async (txn) => {
const current = await txn.get('counter') || 0
await txn.put('counter', current + 1)
})
Alarms
Source: https://developers.cloudflare.com/durable-objects/api/alarms/
Key facts:
- Each DO can have one alarm at a time (
setAlarm() overrides previous)
- Guaranteed at-least-once execution — retried automatically on failure
- Retries use exponential backoff starting at 2s, up to 6 retries
alarm(alarmInfo) receives { retryCount: number, isRetry: boolean }
- Only one
alarm() runs at a time per DO instance
- If DO crashes, alarm re-runs on another machine after short delay
- Calling
deleteAlarm() inside alarm() may prevent retries (best-effort, not guaranteed)
getAlarm() returns null while alarm is running (unless setAlarm() called during handler)
API
ctx.storage.setAlarm(scheduledTimeMs: number): void
ctx.storage.getAlarm(): number | null
ctx.storage.deleteAlarm(): void
async alarm(alarmInfo?: { retryCount: number, isRetry: boolean }): void
Agent Loop Pattern
export class AgentDO extends DurableObject {
async startLoop() {
await this.ctx.storage.put('loopRunning', true)
await this.ctx.storage.setAlarm(Date.now())
}
async stopLoop() {
await this.ctx.storage.put('loopRunning', false)
await this.ctx.storage.deleteAlarm()
}
async alarm(alarmInfo?: { retryCount: number, isRetry: boolean }) {
const running = await this.ctx.storage.get('loopRunning')
if (!running) return
try {
if (alarmInfo?.isRetry) {
console.log(`Alarm retry #${alarmInfo.retryCount}`)
}
await this.runLoopCycle()
} catch (err) {
console.error('Loop cycle error:', err)
}
const config = await this.ctx.storage.get('config')
const interval = config?.loopIntervalMs ?? 60_000
await this.ctx.storage.setAlarm(Date.now() + interval)
}
}
Managing Multiple Scheduled Events
For complex scheduling (multiple events at different times):
async alarm() {
const now = Date.now()
const events = await this.ctx.storage.list({ prefix: 'event:' })
let nextAlarm = null
for (const [key, event] of events) {
if (event.runAt <= now) {
await this.processEvent(event)
if (event.repeatMs) {
event.runAt = now + event.repeatMs
await this.ctx.storage.put(key, event)
} else {
await this.ctx.storage.delete(key)
}
}
if (event.runAt > now && (!nextAlarm || event.runAt < nextAlarm)) {
nextAlarm = event.runAt
}
}
if (nextAlarm) await this.ctx.storage.setAlarm(nextAlarm)
}
DO-to-DO Communication
Agents calling other agents:
async sendToAgent(targetDid: string, message: unknown): Promise<unknown> {
const targetId = this.env.AGENTS.idFromName(targetDid)
const target = this.env.AGENTS.get(targetId)
const response = await target.fetch(new Request('https://agent/inbox', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from: this.did,
message
})
}))
return response.json()
}
Wrangler Configuration
[[durable_objects.bindings]]
name = "AGENTS"
class_name = "AgentDO"
[[durable_objects.bindings]]
name = "RELAY"
class_name = "RelayDO"
[[migrations]]
tag = "v1"
new_classes = ["AgentDO", "RelayDO"]
References