This article is published in English.
How LangGraph Agents Survive Restarts: Checkpoints, Checkpointers, Threads
Learn how LangGraph persistence works: why agents lose everything without it, and how state, checkpoints, checkpointers and thread IDs let a run resume after a crash.
An agent that keeps everything in process memory forgets it all the moment the process stops: a deploy, a crash, an unhandled timeout or even the end of one request is enough to wipe the conversation and every intermediate result. LangGraph addresses this with persistence, a mechanism that records the graph's state after each step so a run can be paused, recovered and continued rather than repeated. This guide builds the mental model from the ground up: what goes wrong without persistence, how LangGraph state relates to checkpoints, what a checkpointer does, how thread IDs keep conversations apart, and where the in-memory checkpointer fits. By the end you will be able to attach persistence to a graph, reason about exactly what gets saved and when, and know why the in-memory option is only a development tool.
Persistence is also the foundation for several features that tend to get discussed separately: human approval steps, tool calling across multiple turns and long-running agents all rely on the ability to stop a graph and pick it up again later. Understanding it first makes those features far less mysterious.
Why agents need state that outlives the process
Think of typing a long document for two hours without saving, and then losing power. The work is simply gone, and you start again from the first page. An agent without persistence is in exactly that position every time it restarts. It has no record of what happened a few seconds earlier, let alone a few days earlier.
In one sentence, persistence means storing an application's state somewhere durable so it can be recovered and continued after the program stops. It works like an automatic save button that fires after almost every step, without anyone having to press it.
This matters more for agents than for classic request/response code because modern agents are rarely a single question and a single answer. They typically:
- carry conversations across hours or days;
- stop and wait for a person to approve an action;
- chain several tool calls to finish one task;
- reason through many steps over a long period;
- have to survive restarts and crashes without losing progress.
RAM is fast, but it is volatile: it is cleared when the process exits. Anything an agent needs beyond the lifetime of one process must be written to durable storage and read back later. In LangGraph, that storage and the machinery that manages it are what "persistence" refers to.
What breaks when an agent has no persistence
The problem is easiest to appreciate through concrete failure modes. Each of the following is routine in production.
Power loss halfway through a long job
An agent is summarizing a 200-page report and has reached page 100 when the machine loses power. Without saved state, there is no record that half the work is done, so the next run begins at page 1.
A routine server restart
The agent runs on a cloud server, and a normal deployment restarts it. Every user who was in the middle of a conversation loses their history. The chatbot no longer even knows a user's name that was given two minutes earlier.
A crash in the middle of a multi-step task
As part of a larger task, the agent calls an external API that times out, and the Python process dies on an unhandled exception. The failed step is lost, but so is everything the agent completed before it.
Workflows that run for hours or days
Some agents are deliberately slow. Picture one that watches stock prices and only acts when a threshold is crossed. If its whole state lives in memory, it cannot be paused, redeployed or moved to another machine without starting over.
Human approval that takes hours
An agent drafts a legal document that a lawyer must approve before it is sent. The lawyer may not look at the queue for six hours. Keeping a process frozen and holding resources for that long is wasteful, and if the server restarts during the wait, the task disappears.
Multi-step reasoning that fails late
Complex agents often split work into a loop of planning, searching, verifying and summarizing. If step 7 of 10 fails, rerunning steps 1 to 6 wastes time, API calls and money.
Why "just run it again" is not a strategy
Restarting from scratch sounds acceptable until you count the costs:
- Money. Each LLM call consumes tokens, so rerunning six successful steps because the seventh failed means paying for them twice.
- Time. Users have to wait while work they already waited for is repeated.
- Trust. A banking assistant that forgets a loan application every time the page refreshes does not feel like a real product.
- Side effects. If an earlier step already sent an email or charged a card, repeating it causes real-world damage. Some steps are not safe to replay.
That last point is the most important one. The goal of persistence is not only to save effort; it is to let an application pause, fail, restart or wait and then continue from where it actually stopped, so completed work stays completed.
A precise definition of persistence
With the problem in view, a more careful definition is useful: persistence is a system's ability to write its internal data, its state, to durable storage so that the data outlives the current run and can be loaded again to continue execution exactly where it left off.
That definition contains three distinct capabilities:
- Saving state: recording what the agent currently knows and what it has done.
- Recovering a previous execution: reading that record back, even after a restart.
- Resuming the workflow: continuing from the recovered point instead of from the beginning.
Temporary memory versus durable storage
A frequent source of confusion is the difference between holding data and persisting it. A regular Python dictionary containing the conversation is temporary memory: when the process ends, the dictionary is gone. Persistence means taking a snapshot of that data and writing it somewhere that survives the process, typically a database. That is the entire idea; the rest of this guide explains how LangGraph implements it.
What persistence buys you in production
Beyond avoiding lost work, persistence changes what kinds of systems you can build.
- Long-running agents. Agents that crawl many pages, process large datasets or wait for external events can be paused and resumed at any time, on any machine that can reach the storage.
- Fault tolerance. A fault-tolerant system keeps behaving correctly when crashes, network failures or timeouts occur. With persistence, a failure costs only the step that was in progress, not the whole task.
- Approval workflows. When a person must review something, the agent can stop in place for as long as needed without losing anything. Building this becomes straightforward once state is durable.
- Recovery without custom code. You do not write special recovery routines. You load the latest saved checkpoint and continue; LangGraph does this for you once persistence is enabled.
- Dependable products. Real users will not accept being asked to start over after every routine deployment. Persistence is much of the difference between a fragile demo and a product.
- Lower cost. Expensive work that already succeeded, such as long generations or slow tool calls, is not paid for again.
- Better experience. People expect a chat assistant to remember the conversation after they close the tab and come back. That memory is persistence at work.
A quick test for whether you need it: if the process restarted right now, would a user be unhappy? If the answer is yes, the graph needs persistence.
State: the thing that actually gets saved
Persistence in LangGraph only makes sense once you understand state, because persisting a graph really means saving its state at well-chosen moments.
A LangGraph application is a graph: a set of steps, called nodes, connected by edges, with data flowing through them. As execution moves between nodes, they need a shared structure to read from and write to. That shared structure is the state. A useful picture is a whiteboard in a meeting room: each node walks up, reads what is there, adds its own contribution and hands over to the next node.
State is usually declared as a typed dictionary. The first step is the import:
from typing import TypedDict
The schema then lists the keys the graph works with and their types. Here the state tracks a message list, the user's name and a step counter:
class State(TypedDict):
messages: list
user_name: str
step_count: int
Because State is a TypedDict, it is simply a dictionary with a fixed set of keys and declared types. Each node receives the current state and returns a partial update, and LangGraph merges that update into the shared state.
Why everything revolves around state
State is the center of a LangGraph application:
- nodes read it to decide what to do;
- nodes write their results back into it;
- edges can route to different nodes based on values in it;
- persistence saves it and restores it.
Once that clicks, persistence reduces to a single sentence: after each step, take a picture of the state and put the picture somewhere safe.
Snapshots after every step
Hold on to this model for the rest of the guide. Each time a node finishes, LangGraph captures the current state and writes it to storage. If the process dies right after the second node completes, the snapshot from that moment still exists, so execution can continue from there rather than from the first node. That snapshot has a name: a checkpoint.
Checkpoints: snapshots of state at one moment
A checkpoint is a snapshot of the graph's state at a specific point in time. The term comes from the same place as in video games: it is a safe spot you can return to instead of replaying the whole level.
What checkpoints make possible
A checkpoint answers one question reliably: what did the state look like immediately after a given step finished? Without checkpoints you only ever know the current state, and only while the program is running. With them, you can inspect any earlier point in a run, and the system can recover to the most recent one after a failure.
Checkpoints are created for you
A detail that surprises many newcomers is that you never save checkpoints by hand. Once a checkpointer is attached to the graph, LangGraph writes a new checkpoint after every super-step. A super-step is one tick of the graph's execution loop; in a simple linear graph it corresponds to one node finishing, while in a graph with parallel branches, all nodes scheduled in the same tick belong to one super-step. You write ordinary graph code, and the saving happens alongside it.
What a checkpoint contains
A checkpoint typically records:
id: a unique identifier, ordered so later checkpoints sort after earlier ones;ts: when the checkpoint was created;channel_values: the state data itself, such as messages and other variables, at that point;channel_versions: internal version counters LangGraph uses to track which parts of the state changed;- metadata: bookkeeping such as which node produced the checkpoint and the step number.
There is no need to memorize this layout. The working definition is enough: a checkpoint is a snapshot of state plus some bookkeeping, recorded at a specific moment. If you want to see how these pieces are stored internally, including pending writes and blobs, there is a deeper walkthrough in Inside LangGraph's InMemorySaver.
A counter, step by step
Consider a graph with one node that increments a number, run three times in succession. After the first run the saved state holds {count: 1}, after the second {count: 2}, and after the third {count: 3}, each as its own checkpoint. If the program crashes immediately after the second checkpoint is written, a restart can continue from {count: 2} without repeating the first two increments.
Checkpointers: the component that saves and loads
If a checkpoint is the snapshot, a checkpointer is the component that takes snapshots, stores them and retrieves them. It is LangGraph's persistence layer.
An analogy that fits well is a camera with a filing cabinet built in. Whenever a node completes, the camera takes a picture of the state and files it. The cabinet can be process memory, a local file or a database, depending on which checkpointer you choose.
Three jobs of a checkpointer
A checkpointer is responsible for:
- Saving state: writing the new snapshot to storage after each step.
- Loading state: reading the most recent snapshot back when the graph is invoked again for the same thread (threads are covered in the next section).
- Restoring execution: handing that snapshot to the runtime so the graph continues from where it stopped instead of starting fresh.
Attaching a checkpointer at compile time
Persistence is switched on when the graph is compiled. You import a checkpointer class and the graph builder; the source labels this snippet as JavaScript, but it is Python:
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
Then you create the checkpointer and pass it to compile(). Note that in the snippet as printed, the comment and the checkpointer = InMemorySaver() assignment have been merged onto one line, which would turn the assignment into part of the comment; in real code they belong on separate lines:
# ... assume `builder` is a StateGraph you've already defined ...checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
That one checkpointer=checkpointer argument enables persistence for the whole graph. Without it, LangGraph stores nothing, and every invoke() begins with empty state.
The resulting cycle is load, run, save, repeated after every step. That is why persistence feels automatic: you never call save or load functions yourself, because the compiled graph does it as part of normal execution.
One caveat is easy to miss. A checkpointer only persists graphs it was passed to. If the same file compiles a second graph without a checkpointer, that second graph has no persistence at all.
Threads: keeping conversations apart
The thread_id value appears throughout LangGraph code, and it deserves a precise explanation. A thread represents one continuous conversation or task. Each thread has a unique ID, and every checkpoint produced for that conversation is grouped under it.
Why every conversation needs its own thread
Imagine a support chatbot serving thousands of customers simultaneously. One customer asks about a refund while another asks about a late delivery. These are separate conversations running in parallel, and mixing them up, for example telling the first customer about the second customer's parcel, would be a serious failure.
Thread IDs prevent that by working like labels on folders. Each checkpoint is filed under exactly one thread ID, so state from different conversations never blends together.
Passing a thread ID in code
The thread is selected through a configuration dictionary. The thread_id sits under the configurable key (this and the next snippet are Python, not plain text):
config = {"configurable": {"thread_id": "customer-a-session-101"}}
That configuration is passed alongside the input on every call:
result = graph.invoke({"messages": [{"role": "user", "content": "Where's my refund?"}]}, config)
Every call to graph.invoke() or graph.stream() takes a config containing the thread_id. LangGraph uses it to:
- look up existing checkpoints for that thread, if there is history to load;
- file new checkpoints under the same ID as the run continues.
Use a different thread_id and you get a fresh, empty conversation, as though the state had been reset, even though the compiled graph and the checkpointer are exactly the same objects.
How threads map to real products
- A chat interface: each chat you open is effectively its own thread, and switching chats is like switching thread IDs. What you said in one conversation does not leak into another.
- A support desk: each ticket can map to one thread ID, keeping that customer's history isolated and easy to retrieve later.
- A personal assistant: an assistant that manages one person's calendar and to-do list might use a single, long-lived thread tied to the user account, so preferences carry across many days.
A practical consequence is that thread IDs should be generated and stored deliberately, for instance derived from a ticket number or a session ID in your own database. If the ID is lost, the checkpoints are still there, but nothing points to them.
One compiled graph, many threads
You do not need a graph per user. One compiled graph can serve any number of threads concurrently; what you need is one unique thread ID per user or per conversation. The graph defines behavior, and the thread defines whose state that behavior operates on.
Following one run from start to crash to resume
Putting state, checkpoints, the checkpointer and threads together gives a complete picture of what happens during execution. The flowchart below (Mermaid syntax, shown as text) traces one run, including a crash and the return path afterwards:
flowchart TD
A[1. Graph starts with invoke] --> B[2. Checkpointer checks thread_id for existing State]
B --> C{State exists for this thread?}
C -->|Yes| D[3a. Load last saved State]
C -->|No| E[3b. Start with fresh empty State]
D --> F[4. Node executes]
E --> F
F --> G[5. State updates in memory]
G --> H[6. Checkpoint saved to storage]
H --> I{More nodes to run?}
I -->|Yes| F
I -->|No| J[7. Return final result to caller]
H -.->|💥 Crash happens here| K[Process restarts]
K --> B
In prose, the sequence is:
- The graph starts. You call
graph.invoke(input, config)with a particularthread_id. - State is created or loaded. The checkpointer checks whether that thread already has checkpoints. If it does, the latest one becomes the starting state; if not, execution begins from empty state.
- A node executes using the state it was handed.
- State is updated. Whatever the node returns is merged into the state.
- A checkpoint is written. The checkpointer stores the new state under the thread ID.
- The next node runs, and steps 3 to 5 repeat.
- A crash happens, say right after the checkpoint for node 2 was written but before node 3 started.
- Execution resumes. Invoking the graph again on the same thread loads the last successfully written checkpoint, the one after node 2, and execution continues with node 3, not node 1.
There is no separate recovery mode to implement. Calling the graph again on the same thread is enough for LangGraph to work out where to continue.
One detail is worth being precise about. To continue a run that was interrupted or failed, you invoke the graph with None as the input and the same config, which tells LangGraph to proceed from the saved checkpoint rather than start a new run. Passing fresh input on an existing thread starts a new run that builds on the saved state: keys with a reducer, such as a message list, accumulate, while plain keys are overwritten by the new value. You can inspect what was saved with graph.get_state(config) for the latest snapshot and graph.get_state_history(config) for the full sequence.
This same mechanism is what allows an agent to stop on purpose, for example to wait for a human decision, and to resume hours or days later on a different machine, provided that machine can reach the same persistent storage. For a hands-on look at that pattern, see Pausing and resuming LangGraph agents with interrupt and Command.
The simplest backend: InMemorySaver
LangGraph does not tie you to one storage system. It supports several backends, meaning places where checkpoints are physically kept, and they differ mainly in durability and in how many processes can share them. The simplest is InMemorySaver.
What it is and how it stores checkpoints
InMemorySaver is a checkpointer that keeps every checkpoint in the RAM of the current Python process, in an ordinary in-memory dictionary indexed by thread ID. There is no file and no database, only a Python object.
The first example uses a typed state with a single count key and one node that adds one to it. The node is wired from START to END, the graph is compiled with an InMemorySaver, and it is invoked on thread-1 with a starting count of 1, which produces 2. The source marks it as JavaScript, but it is Python; also note that it assumes TypedDict, StateGraph, START, END and InMemorySaver were imported earlier:
class StateInt(TypedDict):
count: int
def add_one(state: StateInt) -> dict:
return {"count": state["count"] + 1}
builder = StateGraph(StateInt)
builder.add_node("add_one", add_one)
builder.add_edge(START, "add_one")
builder.add_edge("add_one", END)
memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "thread-1"}}
result = graph.invoke({"count": 1}, config)
print(result) # {'count': 2}
The next snippet is just a caption contrasting a minimal version of the graph with the more realistic one above:
Two ways to write the same graph — minimal vs. real-world.
The minimal variant needs asyncio in addition to the checkpointer and the graph builder:
import asyncio
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
It then uses a plain int as the entire state, registers a lambda as the node, marks that node as both entry and finish point, and runs the graph asynchronously with ainvoke. As printed, several statements have been run together onto single lines (for example the set_finish_point call and the InMemorySaver() assignment), so separate them before running it; the snippet is also Python despite its label:
builder = StateGraph(int)
builder.add_node("add_one", lambda x: x + 1)
builder.set_entry_point("add_one")
builder.set_finish_point("add_one")memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)config = {"configurable": {"thread_id": "thread-1"}}
result = asyncio.run(graph.ainvoke(1, config))
print(result) # Output: 2
Going through the important lines:
InMemorySaver()creates an empty checkpoint store in memory.builder.compile(checkpointer=memory)attaches that store to the graph, which turns persistence on.config = {"configurable": {"thread_id": "thread-1"}}ties the call to one named conversation.graph.ainvoke(1, config)is the async entry point, here started with the integer1as state on thread"thread-1".
Invoking again with the same thread_id works against the checkpoints already saved for that thread instead of an empty history. Keep in mind the distinction from the previous section: in this tiny graph the run has already completed and the state is a single overwritten value, so new input simply replaces it, while None as input would continue an unfinished run.
Strengths
- No setup: no database or external service is needed.
- Very fast, since there is no disk or network latency.
- Well suited to unit tests.
- Convenient for learning and for notebook experiments.
Limitations
- Everything disappears when the process stops. This is plain RAM, which is exactly the problem described at the start of this guide.
- It cannot be shared across processes or servers, because each process has its own memory.
- It is unsafe for production, since an ordinary restart erases every conversation.
When to reach for it
Appropriate uses are:
- local development and debugging;
- automated tests, both unit tests and CI pipelines;
- quick prototypes and notebooks where surviving a restart does not matter.
Anything real users depend on needs a database-backed checkpointer. The LangGraph documentation is explicit that InMemorySaver is intended for debugging and testing, and recommends a durable implementation such as PostgresSaver for production. Setting that up, along with other backends such as SQLite and Redis, custom checkpointers and approval flows built on interrupt() and Command, is the natural next step; a production-oriented example of running LangGraph on Postgres and Redis is covered in Self-hosting a LangGraph agent server.
Key takeaways
- Persistence writes a graph's state to durable storage so a run survives crashes, restarts and long waits instead of starting over.
- Starting over is not just slow: it repeats paid LLM calls and can replay side effects such as emails or payments.
- State is the shared data flowing through the graph, and it is precisely what gets saved.
- A checkpoint is a snapshot of that state after a super-step; a checkpointer creates, stores and reloads checkpoints automatically once passed to
compile(). - A
thread_idisolates one conversation or task, so a single compiled graph can serve many users safely. - Resuming an interrupted run means invoking the same thread with
None; new input on an existing thread starts a new run on top of the saved state. InMemorySaveris ideal for tests and prototypes, but because it lives in process memory, production systems need a database-backed checkpointer.