Persistence and threads
The feature that makes LangGraph more than a nicer loop. Attach a checkpointer and every step is saved, which buys you conversation memory, resumption after a crash, human-in-the-loop and time travel — all from the same mechanism.
Two systems, different scopes
| Checkpointer | Store | |
|---|---|---|
| Scope | one thread | across threads |
| Holds | graph state per step | application data |
| For | conversation, resume, HITL | user preferences, facts |
Checkpointers are short-term and thread-scoped. Stores are long-term and cross-thread — the place a fact about a user survives after the conversation ends. Most people only need the checkpointer at first.
Attaching one
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "conv-42"}}
graph.invoke({"question": "hi"}, config)Two things happened. Every superstep wrote a checkpoint, and the run is
labelled conv-42.
The thread_id is the whole interface. Invoke again with the same one and
the graph continues that conversation, state intact. Use a new one and it
starts fresh. There is no session object to manage — the id is the cursor.
# Same thread: the graph still has the earlier messages
graph.invoke(
{"messages": [
{"role": "user", "content": "and after?"}
]},
config,
)Choosing a backend
| Backend | Use for |
|---|---|
InMemorySaver |
tests, notebooks |
SqliteSaver |
local development, single process |
PostgresSaver |
production |
AsyncPostgresSaver is the one for an async application — mixing the sync
saver into an async graph will block the event loop, which is the same mistake
as any blocking call in async code
(Python Concurrency Models: Processes, Threads, and Asyncio).
Since state is already in Postgres, you get its guarantees for free — backups, replication, and the ability to query runs with SQL. That is a real argument for the Postgres saver in a regulated environment over anything bespoke.
Gotcha: keep
thread_idunder 255 characters. Longer values hit a database column limit and fail at write time, not at validation.
Inspecting and editing state
The checkpointer is not write-only. You can read the current state, list history, and modify it.
snapshot = graph.get_state(config)
snapshot.values # the state dict
snapshot.next # nodes about to run
for past in graph.get_state_history(config):
print(past.config, past.values)update_state writes a new checkpoint by hand:
graph.update_state(config, {"answer": "corrected"})This is the mechanism behind time travel: pick a past checkpoint, update it, and resume from there. The run forks — the original history is untouched, which is what makes it safe for debugging a production thread.
Being able to say “I can replay a failed run from the checkpoint before it went wrong, with one value changed” is a strong answer to “how do you debug an agent”.
What this actually buys you
- Conversation memory without a session store you wrote.
- Crash resumption — the process dies, the state is on disk, the run continues from the last completed step.
- Human-in-the-loop — see Human in the loop. Pausing for hours is just a checkpoint nobody has resumed yet.
- Auditability — every intermediate state is recorded, which in FinTech is a compliance artefact rather than a debugging nicety. See Model governance and responsible AI.
The cost to name
Checkpointing writes the whole state on every superstep. A state holding fifty retrieved documents across twenty steps is twenty copies of those documents.
The fixes are the ones from State and reducers: keep ids in state rather than bodies, and summarise accumulating history. There is also a retention question — checkpoints containing prompts and completions contain PII, so they need the same policy as any other trace store.
Related
Interview angle 5
- “How does LangGraph handle memory?” - a checkpointer writes state after every superstep, scoped to a
thread_id. Invoking with the same id continues that conversation; a new id starts fresh. There is no session object — the id is the cursor. - “Checkpointer or store?” - the checkpointer is thread-scoped short-term state: conversation, resumption, human-in-the-loop. A store is cross-thread long-term data like user preferences that should outlive the conversation.
- “Which backend in production?” -
PostgresSaver, orAsyncPostgresSaverin an async app — the sync one blocks the event loop. State in Postgres inherits backups, replication and SQL queryability. - “How do you debug a bad run?” - read the state history, pick the checkpoint before it went wrong,
update_stateto change one value, and resume. The run forks rather than overwriting, so the original history survives. - “What does it cost?” - the full state is written every superstep, so accumulating document bodies in state multiplies storage by step count. Keep ids in state and fetch bodies in the node, and treat checkpoints as PII-bearing for retention.