← Back to Knowledge
checkpointsqlitepersistence

Swap in RustSQLiteCheckpointer for 5–6x faster checkpointing

Neul Labs · · Level: intermediate · Read: 6 min
TL;DR

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

MetricLangGraph SqliteSaverRustSQLiteCheckpointerSpeedup
Checkpoint serialization (small state)~15 ms0.35 ms43×
Checkpoint serialization (35 KB)~52 ms0.29 ms178×
Checkpoint serialization (235 KB)~206 ms0.28 ms737×
End-to-end PUT+GETBaseline5–6× faster5–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