Persistence
LangGraph provides a layered persistence system built on checkpoints, threads, and a cross-thread memory store.
Why Persistence
| Use Case | Description |
|---|---|
| Human-in-the-Loop | Pause for approval, resume with human input |
| Memory | Retain conversation history across invocations |
| Time Travel | Replay from any prior checkpoint for debugging |
| Fault Tolerance | Resume from last checkpoint after crash |
| Pending Writes | Track in-flight per-node writes for recovery |
Threads
A thread is a unit of execution identified by a unique thread_id in config. All state for a workflow instance is isolated to its thread.
config = {"configurable": {"thread_id": "user-42-conversation"}}
graph.invoke({"message": "Hello"}, config)
# Later -- same thread, state accumulates:
graph.invoke({"message": "What is durable execution?"}, config)
The thread accumulates state across runs. Each invoke with the same thread_id picks up where previous runs left off.
Checkpoints
A checkpoint is a snapshot of the graph state saved at each super-step boundary. It captures everything needed to replay from that point.
StateSnapshot Fields
snapshot = graph.get_state(config)
# snapshot (StateSnapshot):
# .values -- current state dict
# .next -- next nodes to execute (empty if finished)
# .config -- config to resume from this checkpoint
# .metadata -- {source, step, writes}
# .created_at -- ISO timestamp
# .parent_config -- config of parent checkpoint (for history traversal)
# .tasks -- pending Send tasks
Super-Steps
A super-step is one execution of a node (including all its internal operations). A checkpoint is created at each super-step boundary -- when a node finishes or when a task sends to another node.
[checkpoint_0] -> node_A -> [checkpoint_1] -> node_B -> [checkpoint_2]
Pending Writes
Within a super-step, each write to state is tracked as a pending write. If a node fails mid-execution, pending writes are saved so the system knows what partial work was done.
On resume, pending writes from the failed step are replayed to reconstruct accurate in-progress state. This prevents both data loss and duplicate work.
Checkpoint Namespace
Checkpoints use a namespacing scheme to handle subgraphs:
| Namespace | Meaning |
|---|---|
"" (empty) |
Parent graph checkpoint |
"node_name:uuid" |
Subgraph checkpoint for a specific invocation |
This allows the parent graph to maintain its own checkpoint lineage while child graphs checkpoint independently.
Get State
Retrieve current or historical state:
# Current state:
current = graph.get_state(config)
# State history (all checkpoints for this thread):
history = graph.get_state_history(config)
# Filter by metadata:
for checkpoint in history:
print(checkpoint.metadata["step"], checkpoint.values)
Replay
Re-execute from a specific prior checkpoint by providing its config:
history = list(graph.get_state_history(config))
old_checkpoint = history[3] # jump back 3 steps
graph.invoke(None, old_checkpoint.config) # replay from that point
The graph replays from old_checkpoint, producing new checkpoints that fork the history.
Update State
Create a new checkpoint by directly modifying state:
graph.update_state(
config,
values={"messages": [HumanMessage("corrected input")]},
as_node="human_edit" # attribute to a node for lineage
)
This creates a new checkpoint without executing nodes. Useful for corrections, injecting data, or branching the conversation.
Memory Store
The Store provides cross-thread persistence, enabling long-term memory shared across workflow instances.
Store Backends
| Backend | Use Case |
|---|---|
InMemoryStore |
Testing, development |
PostgresStore |
Production, persistent |
AsyncPostgresStore |
Async production |
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
graph = builder.compile(checkpointer=checkpointer, store=store)
Basic Store Operations
Namespace
Data is organized in hierarchical namespaces (tuples):
namespace = ("users", "user-42", "memories")
Put
Store data with a key:
store.put(namespace, "preferences", {"theme": "dark", "language": "en"})
Get
Retrieve by key:
prefs = store.get(namespace, "preferences")
# Returns Item(value={"theme": "dark", "language": "en"}, key="preferences", ...)
Search
Query across namespaces with filters:
results = store.search(
namespace_prefix=("users", "user-42"),
filter={"type": "memory"},
limit=10
)
Accessing Store from Nodes
def my_node(state, *, config, store):
namespace = ("users", config["configurable"]["thread_id"], "notes")
store.put(namespace, "last_session", {"timestamp": datetime.now().isoformat()})
notes = store.search(namespace, limit=5)
return {"notes": notes}
The store is injected into node functions alongside state and config.
Related: Memory, Durable Execution, Interrupts