Stateful Agent Runtime: Durable Objects + Redis

10 min readYaseen Khatib · MERN + AI Architect

Your agent is twenty tool calls into a forty-step refactor when the worker process is recycled, the deploy rolls, or the request times out. Every decision, every file it touched, every dollar of tokens — gone. It starts over from message one, or worse, re-runs side effects it already committed. A Claude model is stateless by design (Lesson 1); making a long-running agent survive a crash is entirely your runtime's job. The answer is checkpointing.

Core Architectural Concepts & Trade-offs

The agent loop (Lesson 4) is a state machine: messages, the current step, accumulated results, token spend. If that state lives only in process memory, a restart erases it. Checkpointing persists the state machine after every meaningful transition so the agent can resume from the last good point instead of the beginning. The unit of recovery is the step: write the new state durably the instant a step completes, and a crash costs you one step of replay, not the whole run.

Where you persist it shapes correctness. Durable Objects give you a single-threaded, single-instance coordinator per agent — strongly-consistent storage with no two-writers race, which is exactly what a sequential agent loop wants. Redis complements it: fast shared state, work queues for fan-out (Lesson 11), distributed locks, and TTL'd scratch space. A common split is a Durable Object as the authoritative per-agent checkpoint and coordinator, with Redis carrying the high-throughput queue and cache traffic around it.

Resumption forces idempotency, and this is the subtle part. If a step issued a refund and then crashed before the checkpoint committed, naive replay issues a second refund. Every side-effecting step needs an idempotency key and a durable record of completion, so replay recognizes "already done" and skips it. Persisting the transcript is the easy half; making external effects exactly-once across a restart is the half that actually keeps you out of the incident channel — and it's the same audit-trail discipline as the guardrail layer (Lesson 10).

The trade-off is latency and complexity versus durability. A checkpoint write after every step adds round-trips to durable storage, so for cheap, idempotent, retry-the-whole-thing tasks it's overkill — just rerun. Checkpointing earns its cost when a run is long, expensive, or has committed side effects you cannot afford to repeat or lose. Match the persistence to the blast radius of a restart: ephemeral for throwaway tasks, durable-and-idempotent for anything that moved money or mutated production.

A Checkpointed Agent Loop

State persists after every step; resume rehydrates it; side effects guard on an idempotency key so replay never double-fires.

durable-agent.ts
// Persist after every step. A crash costs one step of replay, not the run.
type AgentState = {
  runId: string
  step: number
  messages: MessageParam[]
  tokensSpent: number
  done: Set<string>        // idempotency keys of completed side effects
}

export class AgentDO {            // one Durable Object instance per run
  constructor(private store: DurableStore, private redis: Redis) {}

  async resumeOrStart(runId: string, prompt: string) {
    // Rehydrate from the last checkpoint, or start fresh.
    let state = (await this.store.get<AgentState>(runId))
      ?? { runId, step: 0, messages: [{ role: "user", content: prompt }],
           tokensSpent: 0, done: new Set() }

    while (true) {
      const res = await anthropic.messages.create({
        model: "claude-opus-4-8", max_tokens: 1024, tools, messages: state.messages,
      })
      state.messages.push({ role: "assistant", content: res.content })
      state.tokensSpent += res.usage.input_tokens + res.usage.output_tokens

      if (res.stop_reason !== "tool_use") break

      const results = []
      for (const call of res.content.filter((b) => b.type === "tool_use")) {
        const key = `${runId}:${call.id}`
        // Idempotency: skip any side effect this run already committed.
        const out = state.done.has(key)
          ? await this.redis.get(key)
          : await this.execAndRecord(key, call, state)
        results.push({ type: "tool_result", tool_use_id: call.id, content: out })
      }
      state.messages.push({ role: "user", content: results })
      state.step++

      await this.store.put(runId, state)   // ← checkpoint. Durable, after each step.
    }
    return state
  }
}

Kill the process anywhere in that loop and resumeOrStart picks up from the last store.put — replaying at most one step, and the done set guarantees a half-finished side effect never fires twice.

Checkpoint & Resume

step 0step 1step 2step 3✗ crashDurable Object — authoritative checkpoint (state machine)single-instance · strongly consistent · written after every stepRedisqueue · idempotency keys · TTL cacherestart → rehydrate last checkpoint → replay ≤ 1 step
State persists after each step; a crash resumes from the last checkpoint. Idempotency keys make side effects exactly-once.

A durable single agent is ready for the messy reality of production tools. Next, govern many of them at once: enterprise MCP aggregation. Or browse the full roadmap.

Looking to architect a similar system?

Let's ship it at AI-speed.

Start a conversation →