LangGraph state persistence turns ephemeral agent runs into resumable workflows that survive process exits and server restarts. If you’ve built a StateGraph and watched its mutable state disappear when the Python process ended, the missing piece is a checkpointer. This guide shows how to wire durable storage into LangGraph and resume exactly where you left off.
Step 1: Define a StateGraph with explicit state
Start with a typed state contract. LangGraph uses reducers to merge updates, so define how each field accumulates. The example below builds a minimal loop that calls a model node up to three times.
from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
step: int
def call_model(state: AgentState):
# stub node: append a message and bump the counter
return {"messages": [("assistant", "done")], "step": state["step"] + 1}
def should_continue(state: AgentState):
return END if state["step"] >= 3 else "call_model"
graph = StateGraph(AgentState)
graph.add_node("call_model", call_model)
graph.add_edge(START, "call_model")
graph.add_conditional_edges("call_model", should_continue)
At this point graph.compile() produces an app with no durability. Each invocation starts from scratch.
Step 2: Pick a checkpointer backend
LangGraph state persistence relies on the BaseCheckpointSaver interface. Three implementations ship in the standard package:
MemorySaver— process-local dict. Fine for unit tests, useless across sessions.SqliteSaver— file-backed SQLite. Good for single-instance dev and CLI tools.PostgresSaver— PostgreSQL. Correct choice for multi-worker services.
For a runnable local example, use SQLite:
from langgraph.checkpoint.sqlite import SqliteSaver
saver = SqliteSaver.from_conn_string("checkpoints.db")
The connection string can be a relative path; the saver creates the schema on first write. Do not point multiple processes at the same SQLite file with write concurrency — SQLite will serialize writes and stall your agents.
Step 3: Compile the graph with the checkpointer
Compiling with a checkpointer is the only change required to enable LangGraph state persistence. Pass the instance to compile():
app = graph.compile(checkpointer=saver)
Every invoke, ainvoke, or stream call now writes a checkpoint tuple (thread_id, step, state) after node execution. The graph does not need to know the storage details.
Step 4: Run a session with a thread_id
LangGraph scopes state by thread_id inside the configurable config dict. Choose a stable identifier per user session — a UUID, a chat ID, or a composite key.
config = {"configurable": {"thread_id": "session-1"}}
app.invoke({"messages": [], "step": 0}, config)
After this call returns, checkpoints.db contains the post-step-1 state. If the process crashes here, the work is not lost.
Step 5: Resume in a new process
This is the core payoff of LangGraph state persistence: same thread_id, different process. Reconnect the saver, recompile, and read state.
# new process, same machine
saver = SqliteSaver.from_conn_string("checkpoints.db")
app = graph.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "session-1"}}
current = app.get_state(config)
print(current.values) # {'messages': [('assistant', 'done')], 'step': 1}
# continue the loop
app.invoke(None, config)
Passing None as the input tells LangGraph to use the persisted state. The conditional edge re-evaluates from the loaded step count. You can also fork a session by copying the state under a new thread_id.
Step 6: Production hardening
For production-grade LangGraph state persistence, use PostgresSaver with a pooled connection. Create tables explicitly with setup():
from langgraph.checkpoint.postgres import PostgresSaver
import psycopg
conn = psycopg.connect("postgresql://user:pass@localhost:5432/langgraph", autocommit=True)
saver = PostgresSaver(conn)
saver.setup() # idempotent; run once on deploy
A few operational notes from shipping this:
- Connection pooling: wrap the connection in
psycopg.pool. Long-lived agents hold transactions per step; starved pools block resumes. - Concurrent threads:
thread_idis the isolation boundary. Two workers writing the samethread_idwill conflict; assign one thread per conversation and shard workers by hash. - State shape: only JSON-serializable data survives. If you stash Pydantic models or numpy arrays, write a custom reducer and serializer or store references in an external store.
- Checkpoint cleanup: old threads accumulate. Add a cron that deletes rows where
updated_atexceeds your retention window.
If your graph nodes call external models, route those calls through an OpenAI-compatible gateway such as n4n.ai; its automatic fallback across providers keeps a long-running agent alive even when a model backend is rate-limited, while LangGraph state persistence safeguards the conversation.
Verify success
Confirm persistence without guessing. After Step 4, inspect the SQLite store:
sqlite3 checkpoints.db "SELECT thread_id, step, COUNT(*) FROM checkpoints GROUP BY thread_id, step;"
You should see one row for session-1 at step 1. After the resume in Step 5, a second row at step 2 appears.
Programmatically, walk the history to prove the state machine advanced:
for snapshot in app.get_state_history(config):
print(snapshot.step, snapshot.values)
You will get an ordered list of every checkpoint. If that list matches your expected step count after a restart, LangGraph state persistence is working.
Common failure modes
- Forgot
configurable: callinginvokewithoutthread_idwrites to a random ephemeral thread. State vanishes on next run. - Recompiling without saver:
graph.compile()(no arg) drops persistence even if a saver exists elsewhere. Keep one compiledappper process. - Mutating state in place: LangGraph snapshots by serialization. Return new objects from nodes; don’t mutate
state["messages"]directly. - SQLite across containers: a bind-mount or volume is required. A container-local file disappears with the pod.
LangGraph state persistence is a small API surface with large operational leverage. Get the checkpointer right and your agents become debuggable, resumable, and production-tolerant by default.