Swap in RustSQLiteCheckpointer for 5–6x faster checkpointing
from fast_langgraph import RustSQLiteCheckpointer. Pass it to graph.compile(checkpointer=…). Done. Your checkpoints now bypass Python deepcopy entirely.
LangGraph’s SQLite checkpointer is reliable but slow on anything beyond toy workloads. The bottleneck isn’t the SQLite writes — it’s Python’s deepcopy, which runs on every state persistence operation and scales poorly with state complexity. Our benchmarks show deepcopy taking 206 ms on a 235 KB state.
RustSQLiteCheckpointer is a drop-in replacement that keeps the same BaseCheckpointSaver interface but performs serialization in native code.
Adopt it
from fast_langgraph import RustSQLiteCheckpointer
from langgraph.graph import StateGraph
checkpointer = RustSQLiteCheckpointer("state.db")
graph = (
StateGraph(MyState)
.add_node(...)
# ...
.compile(checkpointer=checkpointer)
)
If you’re using MemorySaver or SqliteSaver today, this is a one-line change. No schema migration — it uses the same on-disk format and file layout.
What you gain
| Metric | LangGraph SqliteSaver | RustSQLiteCheckpointer | Speedup |
|---|---|---|---|
| Checkpoint serialization (small state) | ~15 ms | 0.35 ms | 43× |
| Checkpoint serialization (35 KB) | ~52 ms | 0.29 ms | 178× |
| Checkpoint serialization (235 KB) | ~206 ms | 0.28 ms | 737× |
| End-to-end PUT+GET | Baseline | 5–6× faster | 5–6× |
The speedup is not flat — it scales with state size. Small graphs with tiny state see modest gains. Production graphs with accumulated messages, tool outputs, and scratchpads see the dramatic numbers.
Why it’s this fast
The win comes entirely from avoiding Python object overhead. deepcopy walks your state graph recursively, allocates new Python objects, touches the GIL at every layer, and runs pure interpreted bytecode. Rust does the equivalent walk in native code against a flat serialization buffer — no allocations per node, no GIL contention, no interpreter overhead.
The SQLite writes themselves are unchanged. We use the same rusqlite driver under the hood. It’s the serialization path that was the bottleneck, not the storage.
Tuning
For large graphs with heavy checkpointing, the second-order improvement is reducing your checkpoint frequency. RustSQLiteCheckpointer respects the same checkpoint_every and retention settings as the upstream checkpointer. If you’re persisting every super-step and don’t need that granularity, configure it down.
from langgraph.checkpoint.sqlite import ConfigurableCheckpointFieldSpec
graph.invoke(
input,
config={
"configurable": {
"thread_id": "user-42",
"checkpoint_ns": "session",
}
}
)
Migrating an existing database
Because the on-disk format is compatible, there is nothing to migrate. Point RustSQLiteCheckpointer("existing.db") at your current state file and carry on. Existing threads resume normally.
When it does not help
If your state is small (< 1 KB) and your graph only runs a handful of steps per invocation, checkpointing isn’t your bottleneck and you won’t see much difference. Start with the profiler to confirm where your wall clock actually goes before changing anything.
See also
- Benchmarks — full serialization numbers at each state size
- Why Python deepcopy kills LangGraph — the architectural story behind the 737× win
- Checkpoint serialization overhead — the underlying pain point