Skip to content

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-persql
import os
from persql import PerSQL
from 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 persists

Async graphs use the async saver and client:

from persql import AsyncPerSQL
from 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/sdk
import { 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 persists

The 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.

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.

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 latest
FROM checkpoints GROUP BY thread_id ORDER BY 2 DESC LIMIT 20;

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.

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"));

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.

Pair the checkpointer with typed query tools so the same database that holds the agent’s state also answers its questions.