LangGraph checkpointer
LangGraph persists graph state through a checkpointer. langgraph-checkpoint-persql
(Python) and @persql/langgraph (TypeScript) store those checkpoints in a PerSQL
database, so agent threads survive restarts, support time travel, and live in an
isolated SQLite database you can query like any other. LangChain 1.x agents run
on the LangGraph runtime, so this checkpointer is also the persistence path for
LangChain and LangChain.js memory.
pip install langgraph-checkpoint-persqlimport osfrom persql import PerSQLfrom langgraph.checkpoint.persql import PerSQLSaver
client = PerSQL(token=os.environ["PERSQL_TOKEN"])checkpointer = PerSQLSaver(client.database("acme/agent-state"))
graph = builder.compile(checkpointer=checkpointer)config = {"configurable": {"thread_id": "session-42"}}
graph.invoke({"messages": [("user", "hi")]}, config)graph.invoke({"messages": [("user", "remember me?")]}, config) # state persistsAsync graphs use the async saver and client:
from persql import AsyncPerSQLfrom langgraph.checkpoint.persql.aio import AsyncPerSQLSaver
async with AsyncPerSQL(token=os.environ["PERSQL_TOKEN"]) as client: checkpointer = AsyncPerSQLSaver(client.database("acme/agent-state")) graph = builder.compile(checkpointer=checkpointer) await graph.ainvoke(inputs, {"configurable": {"thread_id": "session-42"}})LangGraph.js graphs use @persql/langgraph over the TypeScript SDK:
npm i @persql/langgraph @persql/sdkimport { PerSQL } from "@persql/sdk";import { PerSQLSaver } from "@persql/langgraph";
const client = new PerSQL({ token: process.env.PERSQL_TOKEN! });const checkpointer = new PerSQLSaver(client.database("acme/agent-state"));
const graph = builder.compile({ checkpointer });const config = { configurable: { thread_id: "session-42" } };await graph.invoke({ messages: [["user", "hi"]] }, config);await graph.invoke({ messages: [["user", "remember me?"]] }, config); // state persistsThe saver creates two tables on first use (checkpoints, checkpoint_writes) —
the same layout as LangGraph’s official SQLite saver. Time travel, forks, and
graph.get_state_history() work unchanged. Both packages share the table
layout, but serialized values are language-specific — keep a database’s threads
to one language.
One database per agent
Section titled “One database per agent”A checkpointer keys threads by thread_id inside one database. PerSQL’s shape
lets you go further: give each agent — or each run — its own database, so state,
blast radius, and spend are isolated per agent rather than commingled.
info = client.databases.create(f"agent-{run_id}", ttl_days=1)checkpointer = PerSQLSaver(client.database(f"acme/{info['slug']}"))For ephemeral runs, lease a branch with a TTL and a scoped token instead — the branch (and its checkpoints) clean themselves up.
Inspect state with SQL
Section titled “Inspect state with SQL”Checkpoints are rows. Operational questions that are awkward against an opaque store are one query here — via the console, MCP, or any SDK:
SELECT thread_id, COUNT(*) AS checkpoints, MAX(checkpoint_id) AS latestFROM checkpoints GROUP BY thread_id ORDER BY 2 DESC LIMIT 20;Cross-thread memory
Section titled “Cross-thread memory”Both packages also ship a LangGraph BaseStore for long-term memory shared
across threads — the store interface used by LangMem and deepagents:
from langgraph.store.persql import PerSQLStore # aio: AsyncPerSQLStore
store = PerSQLStore(client.database("acme/agent-state"))graph = builder.compile(checkpointer=checkpointer, store=store)store.put(("users", "u1"), "prefs", {"theme": "dark"})store.search(("users",), filter={"theme": "dark"})import { PerSQLStore } from "@persql/langgraph";
const store = new PerSQLStore(client.database("acme/agent-state"));const graph = builder.compile({ checkpointer, store });Store items are plain JSON (unlike checkpoints), so one database can serve
both languages. Natural-language query search is not supported — structured
filters only; pair PerSQL with a vector layer for semantic recall.
Local tests, no network
Section titled “Local tests, no network”Both SDKs run the same saver against in-process SQLite in local mode:
checkpointer = PerSQLSaver(PerSQL(local=":memory:").database("test/db"))const checkpointer = new PerSQLSaver(new PerSQL({ local: ":memory:" }).database("test/db"));Latency and cost
Section titled “Latency and cost”LangGraph checkpoints once per super-step, so a remote checkpointer adds one HTTP round-trip per step — the same trade-off as LangGraph’s Postgres saver. Checkpoint rows are metered like any other usage: a typical step writes one checkpoint row plus a few pending writes. See pricing for rates.
Next step
Section titled “Next step”Pair the checkpointer with typed query tools so the same database that holds the agent’s state also answers its questions.