n4nAI

How to persist LangGraph state across sessions

Learn how to implement LangGraph state persistence with SQLite and Postgres checkpointers so agent workflows resume cleanly across sessions and restarts.

n4n Team3 min read693 words

Audio narration

Coming soon — every post will get a voice note here.

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_id is the isolation boundary. Two workers writing the same thread_id will 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_at exceeds 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: calling invoke without thread_id writes 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 compiled app per 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.

Tagslanggraphstate-managementcheckpointing

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langgraph for agent workflows posts →