# How Durable Object Hibernation Recovers Session State in Background Agents

> Learn how Durable Object hibernation recovers session state by re-reading SQLite data upon waking, rebuilding WebSocket mappings and re-hydrating services.

- Repository: [Cole Murray/background-agents](https://github.com/ColeMurray/background-agents)
- Tags: internals
- Published: 2026-07-13

---

**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`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/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.

```typescript
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`](https://github.com/ColeMurray/background-agents/blob/main/durable-object.ts) implements this lookup with a fallback cache:

```typescript
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:

```typescript
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`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/durable-object.ts) | Core DO implementation; contains `ensureInitialized()`, `getClientInfo()`, and `handleWebSocketUpgrade()` entry points |
| [`websocket-manager.ts`](https://github.com/ColeMurray/background-agents/blob/main/websocket-manager.ts) | Manages WebSocket lifecycles; implements `recoverClientMapping()` and `persistClientMapping()` for SQLite-backed client tracking |
| [`repository.ts`](https://github.com/ColeMurray/background-agents/blob/main/repository.ts) | SQLite-backed repository providing `getSession()` and other data access methods for session, sandbox, and artifact rows |
| [`websocket-manager.test.ts`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/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 `SessionWebSocketManager` stores and retrieves user-to-socket mappings in SQLite, enabling `getClientInfo()` 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`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/ctx.storage.sql). The `SessionRepository` class in [`repository.ts`](https://github.com/ColeMurray/background-agents/blob/main/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.