Durable Objects Are Your Agent's Memory — If You Design Them Like a Database
Agents

Durable Objects Are Your Agent's Memory — If You Design Them Like a Database

Stateful AI agents on Cloudflare need Durable Objects done right. Session isolation, SQL persistence, and memory patterns that don't contaminate tomorrow's answers.

DO per-Session vs per-User vs per-Tenant: Choosing Your Isolation Model

Before writing a single line of code, decide what scope of memory your agent actually needs:

Per-Session Isolation

Memory lives only for the duration of a single conversation. When the user leaves and returns, they get a fresh slate.

  • Best for: Public-facing agents where you don't want history to influence future users (e.g., customer support bots)
  • How to implement: Create a new DO instance for each connection, destroy when session ends
  • Risk level: Low - minimal chance of cross-contamination
  • Cost: Higher - more DO instances created/destroyed

Per-User Isolation

Each user gets their own persistent memory that spans multiple sessions.

  • Best for: Personal assistants, tutoring systems, productivity tools where context builds over time
  • How to implement: Derive DO ID from user ID (hashed for privacy), reuse same DO across sessions
  • Risk level: Medium - requires careful data leakage prevention between users
  • Cost: Medium - stable number of DOs proportional to user base

Per-Tenant/Organization Isolation

Shared memory within an organization/company, isolated between different customers.

  • Best for: Enterprise software, team collaboration tools, SaaS applications
  • How to implement: Derive DO ID from tenant/org ID, validate access at application layer
  • Risk level: High - potential for cross-tenant data exposure if misconfigured
  • Cost: Low-Medium - number of DOs equals number of tenants

Critical decision: Getting this wrong leads to either creepy privacy violations ("Why does it remember my medical questions from last week?") or useless agents ("It doesn't remember anything I told it yesterday!").

SQL Storage in Agents SDK: What Persists, What Expires

The Agents SDK's built-in SQL interface for Durable Objects behaves differently than traditional databases — understanding these nuances prevents subtle bugs:

What Gets Persisted Automatically:

  • Explicitly committed transactions (await sql.execute(`COMMIT`))
  • Data written within transaction blocks that succeed
  • Changes made outside transactions that aren't rolled back

What Does NOT Persist (Common Gotchas):

  • In-memory JavaScript variables: Anything stored in this or local variables disappears when the DO hibernates
  • Uncommitted transactions: If your Durable Object evicts before COMMIT, changes are lost
  • Temporary tables: Session-specific temp tables don't survive hibernation cycles
  • Prepared statements: Need to be re-prepared after each wake from hibernation

Best Practices for Reliable Persistence:

  1. Wrap all state-changing operations in explicit transactions
  2. Always await commit/rollback before returning from async functions
  3. Initialize database schema in constructor or initialize() method
  4. Use connection pooling patterns if doing frequent DB access
  5. Consider separating hot/warm/cold data storage strategies
class AgentDurableObject extends DurableObject {
  constructor(state, env) {
    super(state, env);
    // Initialize schema when DO is first created
    this.storage.sql.exec(`
      CREATE TABLE IF NOT EXISTS conversations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        session_id TEXT,
        timestamp REAL,
        role TEXT,
        content TEXT
      )
    `);
  }

  async addMessage(sessionId, role, content) {
    // ALWAYS use transactions for persistence
    await this.storage.sql.exec(`
      BEGIN;
      INSERT INTO conversations (session_id, timestamp, role, content)
      VALUES ($1, $2, $3, $4);
      COMMIT;
    `, [sessionId, Date.now(), role, content]);
  }

  async getRecentMessages(sessionId, limit = 10) {
    const result = await this.storage.sql.exec(`
      SELECT * FROM conversations
      WHERE session_id = $1
      ORDER BY timestamp DESC
      LIMIT $2
    `, [sessionId, limit]);

    return result.results;
  }
}

Contrasting Edge Agent Memory with Homelab Patterns

How Cloudflare DO memory compares to your Qdrant/RAG homelab setup:

Cloudflare Workers + Durable Objects

  • Latency: Single-digit milliseconds for edge users
  • Consistency: Strong consistency within DO
  • Scaling: Automatic global distribution
  • Operations: Zero server management
  • Query Power: Limited SQL subset (no joins, complex transactions)
  • Capacity: Constrained by free/paid tier limits
  • Best for: Session state, user preferences, conversation history

Home Lab (VPS + Qdrant + PostgreSQL)

  • Latency: Higher (50-200ms typical)
  • Consistency: Tunable (eventual to strong)
  • Scaling: Manual sharding/replication
  • Operations: Full server/admin responsibility
  • Query Power: Full SQL + vector search capabilities
  • Capacity: Limited only by your hardware
  • Best for: Long-term knowledge bases, embeddings, complex analytics

The hybrid approach that works best for many applications:

  1. Use Durable Objects for short-term context (current conversation, user session)
  2. Use your home lab/Qdrant for long-term knowledge (faqs, documentation, embeddings)
  3. Implement a "memory hierarchy" where important insights from DO get periodically synced to long-term storage
  4. Use TTL policies in DO to automatically expire outdated session data

Hibernation and Cost: Agents That Sleep Between Requests

One of Durable Objects' most powerful (and misunderstood) features is automatic hibernation:

How Hibernation Works:

  • When a DO hasn't received a request for a few seconds, it suspends execution
  • In-memory state is preserved via checkpointing to durable storage
  • CPU usage drops to zero while waiting (you don't pay for idle time)
  • Next request triggers automatic reload and resumption
  • Typical wake time: 10-100ms depending on state size

Cost Implications:

  • You pay primarily for: active compute time + storage
  • Idle DO instances cost almost nothing (just storage)
  • Contrast with traditional VPS where you pay 24/7 regardless of usage
  • Enables cost-effective "always available" agents that only consume resources when used

Designing for Hibernation Resilience:

  1. Never rely on in-memory state persisting between requests
  2. All critical state must be saved to durable storage before returning
  3. Reconstruct any necessary in-memory caches during initialization
  4. Handle potential straggler requests during wake-up period gracefully
  5. Consider implementing warming strategies for critical latency paths
class SmartAgentDO extends DurableObject {
  constructor(state, env) {
    super(state, env);
    this.cache = new Map(); // This WILL be lost on hibernation!
  }

  async initialize() {
    // Rebuild cache from persistent storage on every wake
    const rows = await this.storage.sql.exec(
      "SELECT key, value FROM cache"
    ).results;

    this.cache.clear();
    for (const row of rows) {
      this.cache.set(row.key, JSON.parse(row.value));
    }
  }

  async getCachedValue(key) {
    // Check memory cache first
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }

    // Fall back to persistent storage
    const result = await this.storage.sql.exec(
      "SELECT value FROM cache WHERE key = $1", [key]
    );

    if (result.results.length > 0) {
      const value = JSON.parse(result.results[0].value);
      // Promote to cache for future requests
      this.cache.set(key, value);
      return value;
    }

    return null;
  }

  async setCachedValue(key, value) {
    // Always persist first
    await this.storage.sql.exec(`
      INSERT OR REPLACE INTO cache (key, value)
      VALUES ($1, $2)
    `, [key, JSON.stringify(value)]);

    // Then update cache
    this.cache.set(key, value);
  }
}

Elicitation and Human-in-the-Loop Approvals in MCP Flows

Sometimes the smartest thing an agent can do is ask for help:

When to Elicit Human Input:

  • High-stakes decisions (financial transactions, medical advice)
  • Ambiguous user intent that could lead to harmful actions
  • Need for contextual knowledge the agent doesn't possess
  • Compliance requirements for certain types of decisions
  • When confidence falls below a predefined threshold

MCP-Based Elicitation Pattern:

  1. Agent detects need for human input via confidence scoring or rule matching
  2. Instead of acting, agent returns special "elicitation" response type
  3. Client (UI) presents the elicitation to user for input
  4. User response sent back to agent via standard MCP message
  5. Agent incorporates user input and continues processing
// In your MCP handler
async handleMessage(message) {
  if (this.needsHumanApproval(message)) {
    return {
      type: "elicitation",
      requestId: generateId(),
      prompt: "Please confirm you want to proceed with this action:",
      suggestedActions: ["Approve", "Reject", "Modify"]
    };
  }

  // Normal processing continues here
  return await this.processNormally(message);
}

Designing Effective Elicitation:

  • Clearly state what decision is needed human input and why
  • Provide suggested actions or constraints to guide the user
  • Make it easy to respond (yes/no, multiple choice, bounded input)
  • Timeout unfinished elicitatons to prevent hanging conversations
  • Log all elicitation events for audit and improvement

Anti-Patterns: What NOT to Do with Agent Memory

Learn from others' mistakes — these patterns reliably lead to trouble:

Storing Raw LLM Outputs as Facts

Saving the agent's unverified conclusions or generated content as trusted knowledge.

Problem: Agent hallucinates a financial regulation, stores it as fact, then uses that "fact" to advise dozens of users.

Fix: Only store user-confirmed information or externally validated data as permanent knowledge.

Unbounded Context Accumulation

Letting conversation histories grow indefinitely without summarization or pruning.

Problem: After 100+ message conversations, agent becomes slow, expensive, and loses focus on relevant context.

Fix: Implement sliding window retention or periodic summarization of long conversations.

Cross-Contamination Through Shared State

Using a single DO for multiple users/tenants without proper isolation.

Problem: User A's medical query influences User B's stock advice due to shared context.

Fix: Strictly enforce your chosen isolation model (per-session, per-user, per-tenant) with runtime checks.

Ignoring Data Freshness

Using outdated information without checking expiration or provenance.

Problem: Agent gives legal advice based on superseded regulations because it never checked the "as of" date.

Fix: Timestamp all stored knowledge and implement automatic expiration or periodic refresh cycles.

Reference Architecture: Consult-Booking Agent for ayraix.com

Here's how to apply these principles to a practical example:

Agent Purpose:

Help users schedule consulting appointments with AJ by checking availability, suggesting times, and booking slots.

Data Model:

-- Available time slots (synced periodically from calendar)
CREATE TABLE availability (
  slot_id TEXT PRIMARY KEY,
  start_time INTEGER,  -- Unix timestamp
  end_time INTEGER,
  booked INTEGER DEFAULT 0,
  booker_name TEXT,
  booker_email TEXT
);

-- Conversation context
CREATE TABLE conversation (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id TEXT,
  timestamp INTEGER,
  role TEXT,  -- 'user' or 'assistant'
  message TEXT
);

-- User preferences (learned over time)
CREATE TABLE preferences (
  user_id TEXT PRIMARY KEY,
  preferred_duration INTEGER,  -- minutes
  preferred_days TEXT,         -- CSV: "mon,wed,fri"
  avoid_before INTEGER,        -- hour of day
  avoid_after INTEGER          -- hour of day
);

Key Design Decisions:

  • Isolation model: Per-user (users maintain booking preferences across sessions)
  • Critical data persistence: All booking transactions use explicit transactions
  • Cache strategy: Frequently accessed availability slots kept in-memory with DB backup
  • Hibernation handling: Rebuild availability cache on wake from stored tables
  • Human-in-the-loop: For bookings outside normal hours, elicit confirmation
  • Data freshness: Availability syncs with calendar every 60 seconds
  • Anti-abuse: Rate limiting per user, maximum concurrent bookings

Sample Interaction Flow:

  1. User: "I need to book a 30-minute consultation sometime next week"
  2. Agent: Checks user preferences (learned from past behavior)
  3. Agent: Queries availability for matching slots
  4. Agent: Suggests 2-3 specific times based on preferences + availability
  5. User: Selects preferred option
  6. Agent: Checks if time is outside normal hours → if yes, elicits confirmation
  7. Agent: Books slot in database with transaction
  8. Agent: Updates user preferences based on choice
  9. Agent: Returns confirmation with calendar link

This pattern ensures the agent remembers user preferences (good) without accidentally sharing them between users (bad), maintains consistency despite hibernation (reliable), and knows when to ask for help (safe).

Practical Implementation Tips

From the trenches of building production agents on Cloudflare:

Development Workflow

Use wrangler dev with a local SQLite database that mirrors your DO schema. Seed it with realistic test data to catch edge cases early.

Testing Strategy

Test not just the happy path, but:

  • What happens when the DO hibernates mid-transaction?
  • How does it handle concurrent requests to the same DO?
  • What's the behavior when storage limits are approached?
  • How does it recover from corrupted state (simulate by manually editing the SQLite file)?

Monitoring in Production

Track these key metrics:

  • DO wake time distribution (watch for creep indicating growing state)
  • Transaction success/failure rates
  • Memory usage trends (via occasional introspection)
  • Error rates by type (timeout, validation, storage)
  • Cache hit ratios for in-memory optimizations

Gradual Rollout Approach

Start with:

  • 10% of traffic to new implementation
  • Compare key metrics against legacy system
  • Gradually increase percentage as confidence builds
  • Have instant rollback capability ready

Remember: The most sophisticated agent in the world is useless if its memory can't be trusted. Treat your Durable Objects like the precious, tightly-controlled resources they are — and your users will thank you with their continued trust and engagement.