01. Why Crashes Kill Agents
A long-running agent can crash while it is running. Ordinary programs cannot simply restart from the top. They risk repeating side effects, like sending a message twice. Durable execution solves this. It provides crash-proof execution. The runtime uses a journal to record every operation. This journal holds the planned action, inputs, versions, and approval status. It becomes the source of truth for recovery.
When the agent resumes, it replays recorded results instead of redoing actions. LLM calls and tool calls are not ordinary code. They are replay boundaries. Their results should be saved and reused. Mutating tools need idempotency keys. They need duplicate detection. Without these safeguards, a double send is possible. Raw tools must not be exposed to the model directly. They must be wrapped with duplicate detection and compensation metadata.
Graph checkpoints are powerful but not automatically safe. Each node boundary must be engineered as a replay boundary. That is why restarts can cause trouble. Restarting from the top can also trigger retry storms. Nested retries across services amplify cost and lead to outages. A durable workflow needs global retry budgets. Tracing explains what happened. But a durable journal must decide what may be replayed, skipped, compensated, or resumed. This discipline lets developers focus on what the application should achieve.
A durable agent records each step's planned action, inputs, versions, and result, then wraps mutating tools with idempotency keys and duplicate detection.
class JournalStep:
def __init__(self, step_id, planned_action, inputs, prompt_version,
tool_version, approval_status, result_receipt,
retry_count, error_state):
self.step_id = step_id
self.planned_action = planned_action
self.inputs = inputs
self.prompt_version = prompt_version
self.tool_version = tool_version
self.approval_status = approval_status
self.result_receipt = result_receipt
self.retry_count = retry_count
self.error_state = error_state
def wrap_tool(tool_func):
def wrapped(run_id, step_id, payload):
# check duplicate via receipt for (run_id, step_id)
receipt = get_receipt(run_id, step_id)
if receipt:
return receipt
result = tool_func(payload, idempotency_key=f"{run_id}:{step_id}")
store_receipt(run_id, step_id, result)
return result
return wrapped
Imagine you are building a tower of blocks. Every time you place a block, you take a photo of your progress. If the tower falls, you don’t start from the floor—you look at your last photo and put back exactly those blocks. Durable execution does this for computer programs: it lets them survive crashes by remembering exactly what they did, so they can pick up where they left off instead of starting over.
When a long-running agent runs, it writes down every step in a journal—the planned action, the inputs it used, the versions of tools, and whether a human approved it. That journal becomes the source of truth. If the program crashes, the runtime reads the journal and replays the recorded results instead of redoing the work. The operations that touch the outside world (calling an AI model, sending a message, running a shell command) are marked as “replay boundaries”—their results are saved and reused on recovery. The journal holds fields like the step ID, approval status, and error state, so the runtime knows exactly which steps have finished.
The trickiest part is that not all code is safe to replay. Ordinary code can run again harmlessly, but actions that change the world (like sending an email) must be protected. The runtime wraps these actions with idempotency keys—a unique label for each step—so that when the system restarts, it checks the key and skips actions already done. Without this subsystem, a crash would leave the agent in an unknown state. It might send the same message twice, run the same AI query again (costing money), or lose a human approval. The program would have to guess what happened, leading to errors and wasted work.
The subsystem begins with a durability mode selection: developers invoke a LangGraph graph execution method like graph.stream({"input": "test"}, durability="sync"). Under “sync” durability, the runtime calls a checkpointer conforming to the BaseCheckpointSaver interface to persist the full graph state synchronously before each superstep moves forward. On failure, the process terminates but the last written checkpoint survives. When the agent restarts—potentially on a different machine—LangGraph reads that checkpoint, recreates all state channels, and continues from the interrupted superstep, having already recorded the output of previously completed steps. In contrast, “async” durability writes checkpoints while the next step runs, and “exit” durability writes only when graph execution exits successfully, by error, or by human-in-the-loop interrupt. The ordered mechanism is therefore: choose a durability mode, execute superstep, save checkpoint (synchronously, asynchronously, or only on exit), and on crash resume from the last saved checkpoint.
The invariant the design preserves is crash-proof execution. The source explicitly states: “Durable Execution is crash-proof because it virtualizes execution, enabling it to take place across a series of processes, each of which can potentially run on a different machine. … The application state is recreated in that new process, after which execution will continue as if the failure never happened at all.” This means variables, local state, and the notion of which statements already ran are all durable. The runtime guarantees that no progress is lost beyond the last checkpoint, and side effects are not blindly repeated because each operation’s result is recorded—LLM calls and tool calls become replay boundaries whose outputs are saved rather than re-invoked.
The key trade-off appears in the three durability modes, each rejecting a different cost. The obvious alternative is to forgo checkpointing entirely (the “exit” mode), which offers the best performance for long-running graphs because it writes only on terminal exits. That choice, however, rejects the ability to recover from a process crash mid-execution: “intermediate state is not saved, so you cannot recover from system failures (like process crashes) that occur mid-execution.” The cost avoided by using “sync” instead is the risk of losing intermediate progress and having to restart from the beginning—a restart that might repeat side effects such as sending a notification twice. “Sync” mode pays a performance overhead to guarantee that every superstep’s state is persisted before the next step begins, thereby eliminating the window for data loss. The design deliberately rejects hardware-based fault tolerance (e.g., hot-swappable CPUs) because that “cannot guarantee that execution will continue if that failure does occur” and offers no protection against software bugs; durable execution builds reliability into the software itself.
A concrete failure mode: a long-running agent using durability="exit" crashes while executing step C (for example, while making an LLM call or writing to a file). Because no checkpoint was written for the intermediate step, the operator sees that the graph’s run history ends at step B. On restart, the agent must begin from step A again, re-executing steps A and B, which may cause a duplicate side effect (e.g., a user receives the same email twice). The observable signal is an incomplete run log in the checkpointer storage—only the final checkpoint after step B exists—and a subsequent run that shows multiple invocations of the same LLM call or tool output. With durability="sync", by contrast, the operator would see a checkpoint after step B and also a partially written checkpoint for step C (if the crash occurred after the checkpoint write began), enabling resumption exactly at step C. The exact identifier BaseCheckpointSaver is the interface that any checkpointer must implement; LangGraph provides several installable libraries that conform to it, and the DeltaChannel optimization (storing deltas instead of full state) further reduces storage growth in append-heavy channels.
Failure: Crash mid‑step with async durability
- Trigger – The process crashes while a step is executing and the graph execution method was called with
durability="async". - Guard – No guard is shown; the source explicitly notes that with
"async"there is “a small risk that LangGraph does not write checkpoints if the process crashes during execution.” - Posture – fail‑hard – the run is aborted and no checkpoint is written.
- Operator signal – Silent absence: the checkpoint store lacks the expected checkpoint for that step.
- Recovery – Not specified in the source; the operator must manually restart the agent from the last known good checkpoint or from the beginning.
Failure: Duplicate notification caused by retry
- Trigger – A retry sends the same notification twice because the first send succeeded but the acknowledgement was lost.
- Guard –
idempotency keys(from the Restate description) detect and suppress the duplicate. - Posture – fail‑soft – the duplicate is silently dropped; the workflow continues.
- Operator signal – No log is mentioned; the deduplication is invisible.
- Recovery – Automatic: the idempotency key ensures only one execution.
Failure: Non‑deterministic code inside workflow
- Trigger – The developer places a nondeterministic call (e.g., an LLM invocation) directly inside workflow logic instead of inside an Activity.
- Guard – The “Workflow/Activity split” (Temporal) is the required boundary; the source says deterministic replay works “only if the agent builder respects the Workflow/Activity split”.
- Posture – fail‑hard – the Temporal service detects non‑determinism and fails the workflow.
- Operator signal – Not specified in the source; the operator would observe a workflow failure with an error indicating non‑determinism.
- Recovery – The developer must refactor the agent to move nondeterministic steps into an Activity and restart the workflow.
Failure: Journal storage corruption
- Trigger – A hardware or filesystem error corrupts the journal file (the record of completed operations) while writing.
- Guard – None shown; the journal is the source of truth for recovery, but no corruption guard or checksum is described.
- Posture – fail‑hard – the corrupted journal prevents any recovery; the agent cannot resume.
- Operator signal – A read error from the storage layer or a malformed‑journal indication (not explicitly named in the source).
- Recovery – Not specified; manual restoration from a backup or re‑execution from scratch is required.
Failure: Idempotency key collision
- Trigger – Two distinct operations accidentally receive the same idempotency key (e.g., from a weak random source).
- Guard –
idempotency keysare the mechanism, but the source does not describe a guard against collisions themselves. - Posture – fail‑soft – one operation is incorrectly treated as a duplicate and skipped, silently degrading the result.
- Operator signal – None given; the skipped operation produces a silent absence.
- Recovery – Not specified; manual verification and re‑keying of the affected step may be necessary.