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