LangGraph Incident Response: Read the Thread, Don't Restart
To recover a failed LangGraph run, resume the existing thread by re-invoking the graph with the same thread_id and checkpointer rather than starting a new run; a restart re-executes completed nodes and, without a durable checkpointer, loses the record.
LangGraph on call, a 10-part series.
A restart is the one recovery action in production that can duplicate work and destroy the evidence of what happened, in the same motion, and report neither.
You are on call for a nightly review pass. The process that runs it is back after its scheduled slot, or it looks stuck, and every reflex says restart it.
The reflex is not stupidity. It is the correct move for a stateless service, and the only move available when the sole record of progress lives inside the process that is misbehaving. That is a typical hand-written loop. It keeps its own notion of progress in local variables, so no run can resume and no finished run can be read back.
There is a real incident behind this page, and it is one sentence long: at a national home-services contractor network, a process that restarted after the day's scheduled slot re-ran the whole pass as a new run; the fix reads the durable run record before re-running.
So the first runbook step for a misbehaving LangGraph run is not a restart and not a fix. It is reading the thread. A restart either repeats work the record shows is done or throws away a run the record could have resumed. Reading the thread tells you which of those two you are about to do, before you do it. LangGraph puts that record somewhere an on-call engineer can query it: a checkpointer saves a snapshot at every super-step of a run, keyed by the run's thread id (LangChain checkpointers docs).
Everything below is one decision, made with two reads.
An Empty next Means Two Different Runs, and Only created_at Tells Them Apart
get_state against a thread id with no checkpoint returns an empty snapshot — next empty, created_at None, metadata None — indistinguishable from one never invoked (checkpointers). A correct id with no record means nothing was saved (checkpointers).
Reading a record
With a record present, next is the field you operate on: empty means the run finished, a node name means it stopped before that node. Each pending task carries an error if the node raised and an interrupts entry if it is waiting on a person; read the error from the task, not from next (checkpointers).
A node in next with no error means no failure was recorded, not that nothing is running. LangGraph's graceful-shutdown path drains at the next super-step boundary and saves a resumable checkpoint, so a thread can be mid-flight and quiet at once (fault tolerance).
Checkpoint History Is a Timeline, and a Timeline Is Not a Diagnosis
get_state_history gives a thread's checkpoints most recent first, one per step (checkpointers). The step before the failure usually is not the cause. The hard part is telling symptoms from root causes as an error propagates.
Split the two jobs. Use the history to establish what already happened, and next and the task's error to establish where the run stopped. Use neither for root cause: answer that after recovery, with the run's dependencies in front of you rather than its clock.
Resume Re-Runs the In-Flight Node, Which Is Where the Cheapness Ends
Resume in LangGraph is cheap in the currency people count: calling the graph again on the same thread id with no input picks up from the last checkpoint, and nodes whose writes were saved are not re-run (checkpointers). The docs put that unqualified, but it is the common case, not a guarantee.
When a step fans out across parallel nodes, a sibling can return within roughly a millisecond of the failing node's raise; the step is cancelled and the executor shuts down before the sibling's write is submitted. The node returned successfully, its result was never saved, and it reappears in next and runs again on resume.
If that node calls the model API, re-running it is not a replay — nodes after the checkpoint re-execute, including model calls, which may return different results (use time travel). Yuan et al. (2025) measure up to 9% accuracy variation and up to 9,000 tokens of response-length difference under bfloat16 greedy decoding when batch size, GPU count, or GPU type changes (Yuan et al., 2025).
Side effects before an interrupt should be idempotent, because an interrupt resumes by re-running its node (interrupts). Nobody should promise exactly-once here.
Your Durability Mode Decided Months Ago Whether This Runbook Exists
Checkpoint behaviour in LangGraph is governed by three settings. In "exit" mode, checkpoints are written only when the run ends — successfully, with an error, or at an interrupt. "async" is the default and writes in the background, with a small risk of losing the latest one on a crash. "sync" writes each checkpoint before the next step begins (checkpointers; thinking in LangGraph).
MemorySaver / InMemorySaver keep checkpoints in RAM and lose them on restart; production wants a checkpointer backed by the database the service already runs (persistence).
"We use a checkpointer" is not the same claim as "the record survives a restart." With an in-memory checkpointer, the page after a restart looks exactly like the unknown-thread case: empty next, no history, and you will hunt a thread-id typo that does not exist (persistence).
Nor is async lossless: "small" is a probability, not a promise. The default is fine for most graphs and the window is nonzero (checkpointers).
The Run Record Already Answers the Question You Were About to Buy a Tracing Stack For
Canopy handles over 1 billion traces per day at Facebook (Kaldor et al., 2017), but answers questions across services, not one run's state. AgentSight notes that existing tools see either an agent's intent through prompts or its actions through system calls, and cannot correlate the two; its answer is eBPF instrumentation outside the application, at under 3% performance overhead (Zheng et al., 2025). A LangGraph checkpoint is that correlation from the inside: one snapshot carries the step number, the source, the state, and the pending tasks with their errors (checkpointers). For the on-call question — where is this run, and what has it already done — you need the thread id and two calls.
Wink found misbehaviours in about 30% of production coding-agent trajectories; a lightweight asynchronous nudge resolved 90% of those requiring a single intervention, across more than 10,000 real-world trajectories (Nanda et al., 2026). The bet that pays is intervening on the trajectory you have.
If State Persistence Isn't Asserted in CI, It Isn't Guaranteed in Production
Teams that never hit this incident assert the property in a test suite already running. monday's team, building customer-facing service agents on LangGraph and LangSmith, runs two-tiered validation per example: deterministic hard assertions that the agent adheres to the response schema and maintains state persistence via checkpointer/storage, followed by an LLM-as-judge pass against a golden dataset. The harness runs with concurrent execution: 18.60s parallel-plus-concurrent against 162.35s sequential, an 8.7x speedup, over 20 sanitised IT tickets (LangChain / monday Service). If you can assert a schema, you can assert that a thread survives a restart.
ReliabilityBench (Gupta, 2026) evaluates agents over 1,280 episodes across four domains, measuring consistency under repeated execution and fault tolerance under controlled tool and API failures. Semantic perturbations alone drop success from 96.9% at ε=0 to 88.1% at ε=0.2, and rate limiting is the most damaging fault in their ablations (Gupta, 2026).
"It worked yesterday" is not evidence about tonight.
Some Runs Deserve a Restart, and the Honest Part Is Saying So
Read-before-restart is not read-instead-of-restart. Restart when there is no record — an in-memory checkpointer after a process restart, or a crash under "exit" durability — and record it as a fresh run. Restart when the saved state is the problem: state that will fail identically on the next resume, or a checkpoint whose state shape the current code no longer understands. Calling the graph with fresh input starts the whole graph again, even on a finished thread, and a fresh run under a new thread id finds no record at all. Restart is the argument you pass, which is why the scheduler — not the framework — decides whether a restart is a new run. LangGraph does not prevent duplicate runs; it gives you a record that lets you decide well.
Four Reads, One Decision
The procedure is four questions in order. First, is there a record? Check created_at; empty means check the thread id first. Second, is next empty? Then the run finished. Third, does next name a node whose task carries an error? Fix the cause, then resume on the same thread id with no input: the failed node re-runs, saved nodes do not. Fourth, does next name a node with no error? Confirm the process is gone, check whether the external write landed, then resume with no input.
Here is the read, in ten lines:
cfg = {"configurable": {"thread_id": "nightly-review-pass"}}
snap = graph.get_state(cfg)
if snap.created_at is None:
print("no checkpoint recorded for this thread")
else:
print("next:", snap.next, "| step:", snap.metadata["step"], "| at:", snap.created_at)
for t in snap.tasks:
print(" pending:", t.name, "| error:", t.error, "| interrupts:", len(t.interrupts))
for s in graph.get_state_history(cfg, limit=5):
print(" ", s.created_at, s.metadata["step"], s.metadata["source"], s.next)
It ran against a three-node toy graph: a crash mid-run printed next: ('b',) | step: 1, and the same thread resumed with no input finished at next: () at step 3.
Three properties decide whether the next page is quick or a guess. The thread id must be derivable from the work, not the process. The durability mode must be known before the page. And the scheduler must resume before it restarts.
API names used above, from the LangGraph MemorySaver persistence page and the checkpointers page: InMemorySaver, thread_id, get_state, get_state_history, next, created_at, metadata, error.
LangGraph Incident Response: FAQ
In LangGraph, a thread groups checkpoints saved during a conversation or run so execution state can be persisted and resumed. Checkpoints are saved snapshots of graph state at each step, stored by a checkpointer, enabling resuming, replaying, or inspecting a thread. With a checkpointer configured, LangGraph execution resumes from a saved checkpoint on the same thread instead of starting over.
Practical takeaways
- Check
created_atbeforenext: an unknown thread and a finished run both return an emptynext. - Read the error from the pending task, not
next. - Assume the in-flight node re-runs from its start; idempotency keys or an external check are the only guard.
- Do not call resume a replay: re-executed model calls may return different results (use time travel).
- Confirm durability mode and checkpointer backend early;
asyncis the default with a nonzero crash window, and in-memory loses everything (persistence). - Assert state persistence in CI (LangChain / monday Service).
- Derive the thread id from the work, not the process.
