← Back to blog
Agent Orchestration·October 20, 2026·12 min read

Durable Execution for AI Agents: How LangGraph, Temporal, Inngest, and DBOS Actually Survive a Crash

AI agent loops run long and cost real money per step, so a naive retry after a crash re-pays for work you already bought. Here is how four durable execution frameworks checkpoint that work, and exactly where each one's recovery model breaks if you trust the default settings.

Your research agent runs forty steps: search, open a dozen pages, summarize each one, cross-check the claims, write the report. By step thirty you've spent real money on model calls and burned several minutes of wall-clock time waiting on other people's APIs. Then the process gets killed — an OOM eviction, a deploy, a flaky dependency — and a naive retry starts the whole thing over. The second run searches again, gets a slightly different set of results because the web changed in the last ten minutes, and produces a different report than the one that died. You've now paid twice for a job that didn't even repeat itself.

This is the problem durable execution frameworks exist to solve, and over the last two years it's become one of the more consequential infrastructure decisions in agent architecture — alongside how you manage context or route tool calls. Four tools dominate the conversation: LangGraph's checkpointer, Temporal, Inngest, and DBOS. They all promise the same outcome — a crashed agent resumes instead of restarting — but they get there through genuinely different mechanics, and each one's recovery guarantee breaks in a different place if you don't understand how it actually works.

Why Retrying From Scratch Doesn't Work for Agents

Ordinary retry logic assumes three things about the job you're retrying: it's cheap to redo, it's safe to redo (no side effects you'd duplicate), and redoing it produces the same result. A cron job that re-reads a file and re-computes a total satisfies all three. An agent loop satisfies none of them.

Every model call costs real tokens, so repeating five already-finished calls isn't free — it's five more invoices. Tool calls frequently have side effects: sending a notification, writing a database row, charging a card. Re-running them on retry doesn't just waste money, it can double the real-world action. And because an agent's path through its loop is chosen by the model at runtime rather than fixed in code, a second attempt at "search the web for X" can legitimately return different results than the first attempt did, which means the retried run isn't even equivalent to the one that crashed — it's a different run that happens to share a prompt.

Durable execution frameworks exist to break the loop into units small enough that a crash only costs you the one unit that was mid-flight, not everything before it.

The Shared Idea: Checkpoint the Step, Not the Function

Underneath their different branding, all four tools do the same basic thing: they let you mark a unit of work — one model call, one tool call — as a step. The first time a step runs, its result gets persisted somewhere durable. If the process dies and the job resumes, a step that already has a saved result doesn't execute again; the framework hands back what it already has and your code moves on as if the call had just returned.

The entire design space lives in three questions: where does that persisted result get written, how does your code get back to where it left off, and what does that imply about what you're allowed to put inside — or outside — a step. The four frameworks answer all three differently.

LangGraph: the checkpointer you already compile with

If your agent is built as a LangGraph graph, durability is a checkpointer object you attach at compile() time, typically backed by Postgres or another store you already run. LangGraph persists the graph's state after each node finishes, and three durability modes trade safety for speed: sync writes before the next node starts, exit writes only once at the very end, and async — the default — writes in the background while the next node is already running.

That default is the sharp edge. LangChain's own documentation is upfront that async carries a small risk of losing the last write if the process dies before the background write lands, which means the node you just paid for can vanish on exactly the kind of crash you bought a checkpointer to survive. The other constraint is granularity: a checkpoint happens per node, so if you pack four tool calls into a single node, a crash anywhere inside it re-runs all four, not one.

Inngest: step.run() and re-invoking the whole function

Inngest takes a different shape: there's no separate graph to declare. You write an ordinary function and wrap each unit of work in step.run("name", fn). The mechanism underneath is re-invocation, not resumption-in-place — every time a step completes, Inngest calls your function again from the top, but any step.run() call whose result is already saved returns that cached result immediately instead of re-executing. The function body runs many times over the life of a run; the expensive or side-effecting work inside each named step runs exactly once.

That has two consequences worth knowing before you're debugging them in production. First, step results are keyed by the name string you pass to step.run(), so renaming a step in a deploy orphans every run that's currently mid-flight waiting on the old name. Second, anything you write outside a step.run() call re-executes on every single pass through the function — a timestamp, a random seed, an un-wrapped API call — so code that needs to stay consistent across a resume has to live inside a step, even if it isn't the part you think of as "the expensive part." In exchange you get primitives like step.waitForEvent(), which suspends a run for hours or days waiting on a human approval without holding a worker open the whole time.

DBOS: durability as a library, not a service

DBOS skips the separate orchestrator entirely. It's a library: you annotate your functions, and DBOS writes workflow and step state directly into tables inside the Postgres database your application already uses, then polls those tables for pending work from whichever process happens to be running DBOS.

The payoff is consistency most of the others can't offer without extra plumbing: because the workflow checkpoint and your actual business-data write can land in the same Postgres transaction, there's no window where "the checkpoint saved but the real write didn't" or vice versa — the kind of split-brain that LangGraph, Inngest, and Temporal all have to solve with an outbox table and a draining job, since each of them writes workflow state somewhere separate from your application data. The cost is that workflow traffic — queue polling, checkpoint writes, history reads — now competes for connections on the same database your product serves user requests from, and recovery correctness hinges on giving each run a stable workflow id; get that wrong and a restart can launch a second, duplicate execution alongside the one that's still stuck.

Temporal: replay from an event history, not a cache lookup

Temporal is architecturally the odd one out. On recovery, a Temporal worker doesn't resume mid-function — it re-executes your workflow function from the very beginning, and that replay has to deterministically reproduce the same sequence of commands (call this Activity, sleep this long, wait for this signal) that the original run issued. Temporal compares each command the replay generates against the event history it already recorded; a match means it hands back the historical result instead of re-executing the Activity, a mismatch raises a non-determinism error and the workflow fails.

This is why Temporal draws a hard line between workflow code, which must be deterministic, and Activities, which do the actual non-deterministic or side-effecting work — the model call, the tool call, the HTTP request. Put a direct network call, Date.now(), or a random number straight into workflow code and the replay produces a different command stream than history recorded, breaking the exact guarantee you installed Temporal to get. In exchange for that discipline, Temporal is the only one of the four that treats workflow versioning as a first-class, mandatory concept — a breaking change to a long-running workflow's shape has to be declared, not silently shipped — and it's built for the case where a lost run is a genuine business incident, not an inconvenience. It's also the only one that requires you to run (or pay someone to run) a cluster.

What "One Step Per Call" Actually Looks Like

The pattern is the same regardless of which framework you pick: wrap the call that costs money or has a side effect, not the loop around it.

for (let i = 0; i < MAX_ITERATIONS; i++) {
  const response = await step.run(`think-${i}`, () =>
    model.call({ messages, tools })
  );

  if (!response.toolCalls.length) {
    return response.text; // model answered — loop ends
  }

  for (const call of response.toolCalls) {
    const result = await step.run(`tool-${call.name}-${i}`, () =>
      executeTool(call.name, call.input)
    );
    messages.push(toolResultMessage(call.id, result));
  }

  messages.push(assistantMessage(response));
}

Every step.run() boundary is a line that a crash can't cross twice. Wrap the entire for loop in one step instead, and you've built a retry queue with extra ceremony — a crash at iteration thirty-eight still re-pays for the first thirty-seven.

Original run

Think 1 checkpointed Act: search checkpointed Think 2 checkpointed Act: fetch page checkpointed Think 3 crash — pod killed

After restart — same five steps, replayed

Think 1 from checkpoint · $0 Act: search from checkpoint · $0 Think 2 from checkpoint · $0 Act: fetch page from checkpoint · $0 Think 3 re-executed · full cost resume

Only the step in flight at crash time gets re-paid — the other four are read back, not recomputed.

Where the Four Actually Differ in Practice

FrameworkState lives inRecovery mechanismNeeds a cluster?Sharpest gotcha
LangGraphA checkpointer backend you configure (often Postgres)Resume from the last saved nodeNo — it's a libraryDefault async durability can silently drop the last node's write on crash
InngestInngest's platform (or self-hosted)Re-invoke the function; replay cached step results by nameNo if hosted; yes (several services) if self-hostedRenaming a step orphans every run currently in flight
DBOSYour own Postgres, in dedicated tablesPoll workflow/step tables; resume by workflow idNo — it's a libraryA missing or reused-incorrectly workflow id can spawn a duplicate run
TemporalAn event history inside a Temporal clusterReplay workflow code; diff emitted commands against historyYes — self-hosted or Temporal CloudNon-deterministic code inside the workflow function breaks replay outright

None of these guarantees hold by default the way the marketing copy implies. LangGraph needs durability="sync" turned on explicitly if you can't tolerate losing the last node on crash. Inngest needs you to never rename a step name a production run depends on. DBOS needs a workflow id strategy you've actually tested under a kill. Temporal needs workflow code that's genuinely free of clocks, randomness, and direct I/O — which is a real constraint on how you're allowed to write the orchestration logic, not just a suggestion.

Where Teams Get the Granularity Wrong

The most common mistake is wrapping the entire agent loop in a single step or Activity, which checkpoints nothing that matters — the crash still re-executes every call inside it, because from the framework's point of view it was all one indivisible unit of work. The fix is mechanical: one step per model call, one step per tool call, no exceptions, even when it feels like overkill for a cheap lookup.

The second mistake is never actually testing recovery. It's easy to read a framework's docs, assume the guarantee holds, and find out otherwise the first time a real crash happens in production. The only way to know what your step boundaries actually save is to kill a running process mid-loop in staging — a literal kill -9 with no graceful shutdown — and look at what gets replayed versus what gets repaid. Every one of the four tools behaves differently under that test than its documentation summary suggests, because the summary describes the mechanism, not your code's specific boundaries.

The third is treating step identity as stable across deploys when it isn't. Rename a step, reorder a branch, or change what a node does internally, and Inngest's name-keyed memoization and LangGraph's node-keyed checkpoints can both silently stop finding the state they expect — which is exactly the failure Temporal's mandatory versioning exists to force you to confront instead of discover.

This same step-boundary discipline applies regardless of how an agent's tools are wired up — whether they're hand-written functions or calls routed through an MCP server, the tool call is still the unit of work a crash can repeat, not the transport it travels over.

The Takeaway

Durable execution doesn't eliminate the cost of a crash — it relocates that cost to wherever you drew the step boundary. Draw it around every call that costs money or has a side effect, verify it with an actual kill signal rather than trusting the documentation, and choose the framework based on where your team's state and operational capacity already live — a graph you've already built, a Postgres you already run, or headcount free to operate a cluster — rather than on which one has the best landing page. The four tools agree on the goal; they disagree, in very specific and testable ways, on what you have to get right to actually reach it.

#agent-orchestration#durable-execution#temporal#inngest#dbos#langgraph

Related reading

Agent Orchestration
LangGraph, CrewAI, OpenAI Agents SDK, Claude Agent SDK: Four Different Bets on How Agents Should Talk to Each Other
Agent Orchestration
Pipeline, Supervisor, or Mesh: Where Each Multi-Agent Orchestration Pattern Actually Breaks
Agent Memory
Letta, Mem0, and LangGraph Stores: Three Different Answers to Where Agent Memory Lives