How Durable Object Hibernation Recovers Session State in Background Agents
Durable Object hibernation recovers session state by re-reading persisted SQLite data when the DO wakes, rebuilding WebSocket client mappings and re-hydrating in-memory services from the stored database.
The ColeMurray/background-agents repository implements resilient session management using Cloudflare Durable Objects. When a DO hibernates due to inactivity, it loses all in-memory state but retains its SQLite storage, requiring a specific recovery protocol to restore session continuity when the agent resumes.
Understanding Durable Object Hibernation and State Persistence
Cloudflare Durable Objects automatically hibernate when they become idle, discarding the in-memory JavaScript heap while preserving the SQLite database accessible via ctx.storage.sql. This architecture separates compute from storage, allowing the DO to resume weeks later while maintaining transactional consistency.
In packages/control-plane/src/session/durable-object.ts, the SessionDO class orchestrates this lifecycle. The critical insight is that all mutable state must be written to SQLite before the DO sleeps, and all services must be reconstructed from that data upon waking.
The Three-Step Recovery Process
When a client reconnects or the sandbox restarts, the DO executes a deterministic recovery sequence that transforms the raw SQLite rows back into functional runtime objects.
Step 1: Re-initializing the SQLite Schema
The ensureInitialized() method serves as the entry point for every DO operation, guaranteeing that the database schema exists before any state access occurs.
private ensureInitialized(): void {
if (this.initialized) return;
initSchema(this.sql); // (re)creates tables if missing
this.initialized = true;
const session = this.repository.getSession(); // reads persisted rows
const sessionId = session?.session_name ?? session?.id ?? this.ctx.id.toString();
this.log = createLogger("session-do", { session_id: sessionId }, parseLogLevel(this.env.LOG_LEVEL));
this.wsManager.enableAutoPingPong();
}
This method runs initSchema(this.sql) to ensure tables exist, then immediately queries the SessionRepository to retrieve the session row and establish logging context.
Step 2: Recovering WebSocket Client Mappings
The SessionWebSocketManager maintains a lightweight mapping of connected clients in SQLite. When a WebSocket connection arrives after hibernation, the DO cannot rely on in-memory caches and must reconstruct the ClientInfo object from the database.
The getClientInfo() method in durable-object.ts implements this lookup with a fallback cache:
private getClientInfo(ws: WebSocket): ClientInfo | null {
// 1️⃣ Try the in‑memory cache first.
const cached = this.wsManager.getClient(ws);
if (cached) return cached;
// 2️⃣ If the DO just resumed, look up the stored mapping in SQLite.
const mapping = this.wsManager.recoverClientMapping(ws);
if (!mapping) {
this.log.warn("No client mapping found after hibernation, closing WebSocket");
this.wsManager.close(ws, 4002, "Session expired, please reconnect");
return null;
}
// 3️⃣ Build a fresh ClientInfo object from the DB row.
const clientInfo: ClientInfo = {
participantId: mapping.participant_id,
userId: mapping.user_id,
name: resolveParticipantName(mapping),
avatar: getAvatarUrl(mapping.scm_login, resolveScmProviderFromEnv(this.env.SCM_PROVIDER)),
status: "active",
lastSeen: Date.now(),
clientId: mapping.client_id ?? `client-${Date.now()}`,
ws,
};
// 4️⃣ Re‑cache for fast future look‑ups.
this.wsManager.setClient(ws, clientInfo);
return clientInfo;
}
If recoverClientMapping() returns null, the connection is terminated with a 4002 "Session expired" error, forcing the client to establish a fresh session.
Step 3: Restoring Higher-Level Session State
After client authentication succeeds, the DO restores the complete session context through SessionRepository.getSession(). This method retrieves:
- The current session row and metadata
- Sandbox state and configuration
- Pending artifact queues
- Message queues for background processing
All services—including the message queue, sandbox lifecycle manager, and presence service—receive this refreshed state and continue operation as if the DO had never paused.
Persistence Patterns for Continuous Operation
To ensure seamless recovery, the system writes client mappings to SQLite at the moment of connection acceptance. In handleSubscribe(), the DO calls:
await this.wsManager.acceptClientSocket(server, wsId);
this.ctx.waitUntil(this.wsManager.enforceAuthTimeout(server, wsId));
// After the participant is identified …
this.wsManager.persistClientMapping(wsId, participant.id, data.clientId);
This eager persistence guarantees that even if the DO hibernates milliseconds after accepting a socket, the mapping survives in ctx.storage.sql and can be recovered during the next wake cycle.
Key Source Files and Architecture
The hibernation-recovery logic spans four critical files in the packages/control-plane/src/session/ directory:
| File | Role |
|---|---|
durable-object.ts |
Core DO implementation; contains ensureInitialized(), getClientInfo(), and handleWebSocketUpgrade() entry points |
websocket-manager.ts |
Manages WebSocket lifecycles; implements recoverClientMapping() and persistClientMapping() for SQLite-backed client tracking |
repository.ts |
SQLite-backed repository providing getSession() and other data access methods for session, sandbox, and artifact rows |
websocket-manager.test.ts |
Unit tests verifying the hibernation-recovery path, including recoverClientMapping() behavior and token handling |
Summary
- Hibernation discards memory, not storage: Cloudflare Durable Objects lose in-memory state but retain the SQLite database accessed via
ctx.storage.sql. - Recovery is data-driven: The DO reconstructs its runtime state by re-reading persisted rows rather than maintaining live objects.
- Client mappings are critical: The
SessionWebSocketManagerstores and retrieves user-to-socket mappings in SQLite, enablinggetClientInfo()to rebuild connections after hibernation. - Schema initialization guards against cold starts:
ensureInitialized()guarantees table existence before any state-dependent operations execute.
Frequently Asked Questions
What happens to in-memory state during Durable Object hibernation?
All in-memory JavaScript objects, variables, and class instances are discarded when Cloudflare hibernates a Durable Object. Only the SQLite database accessed through ctx.storage.sql persists across hibernation boundaries.
How does the system handle client reconnections after hibernation?
When a client reconnects, the DO looks up the stored mapping in SQLite using recoverClientMapping(). If found, it reconstructs the ClientInfo object and re-caches it; if not found, it closes the WebSocket with a "Session expired" error code 4002.
Where is session data stored to survive hibernation?
Session data, client mappings, and queue states are stored in the Durable Object's SQLite database via ctx.storage.sql. The SessionRepository class in repository.ts manages all reads and writes to this persistent storage.
What triggers the recovery process when a DO resumes?
Any incoming request—such as a WebSocket connection or HTTP call—triggers ensureInitialized(), which checks the this.initialized flag. If the DO just woke from hibernation, this method re-initializes the schema, restores the logger, and enables auto-ping/pong before handling the request.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →