Tip
Wondering when you'd actually use checkpoints or stores? Read README-USECASES-EXAMPLE.md — a source-verified guide mapping real-world scenarios (human-in-the-loop approvals, crash recovery, semantic memory) and production case studies (Replit, Klarna, Uber, LinkedIn) to the right persistence mechanism.
Runnable, deterministic examples that show the difference between LangGraph
checkpoints (thread-scoped graph state) and stores (cross-thread,
long-term memory), using real StateGraph, InMemorySaver, and
InMemoryStore code, with tests and CI.
No LLM calls and no API keys: everything runs offline. Every output quoted
below is captured from a real run by
scripts/generate_artifacts.py, with only
volatile values (checkpoint ids, timestamps) redacted.
| Question | Checkpoints | Stores |
|---|---|---|
| What is saved? | Graph state snapshots | Application-defined key-value data |
| Scope | One thread_id |
Cross-thread namespace, e.g. (user_id, "profile") |
| Who writes it? | LangGraph runtime, via the checkpointer | Your graph nodes / app code |
| Best for | Resume, chat continuity, time travel, interrupts, fault tolerance | User facts, preferences, memories, shared knowledge |
thread_id ──▶ checkpointer ──▶ "Where is this graph thread right now?"
user_id ──▶ store ──▶ "What durable facts do we know about this user?"
Use checkpoints to save where this graph thread is. Use stores to save durable memory outside the thread.
flowchart TD
Q{What should the agent remember?}
Q -->|"continue where this conversation left off:<br/>resume, retries, time travel, interrupts"| CP["Checkpointer<br/>scoped by thread_id"]
Q -->|"recall facts in new conversations:<br/>preferences, profiles, shared knowledge"| ST["Store<br/>scoped by namespace, e.g. (user_id, 'profile')"]
Q -->|"shipping a production agent"| BOTH["Both:<br/>compile(checkpointer=..., store=...)"]
The same decision tree is printed at the end of demo all. For the
real-world scenarios behind each branch — human-in-the-loop approvals, crash
recovery, semantic memory, and who runs this in production — see
README-USECASES-EXAMPLE.md.
One command — creates .venv, installs, lints, tests, and runs every demo:
./setup_and_run.shOr step by step:
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
python -m checkpoints_vs_stores.demo all # or: lg-memory-demo all
pytestdemo all renders a small terminal dashboard: the checkpoint and store demos
side by side (stacked on narrow terminals), the combined demo below, and the
decision tree last. Use --plain for flat text or --json for
machine-readable output.
Or with the Makefile: make install, make demo, make test,
make artifacts.
python -m checkpoints_vs_stores.demo checkpointOutput (from
artifacts/sample-output/checkpoint_demo.txt):
thread-alpha / invoke #1:
Checkpoint now has 1 durable-in-thread fact(s).
thread-alpha / invoke #2:
Your name is Ada. I know because this thread has a checkpoint.
thread-fresh / invoke #1:
I don't know your name in this thread.
The second invoke on thread-alpha recalls Ada from checkpointed thread
state. thread-fresh knows nothing, because it is a separate checkpoint
lineage.
flowchart LR
U[User message] --> G[LangGraph]
G --> N1[Node: extract fact]
N1 --> N2[Node: answer]
N2 --> C[(Checkpointer)]
C --> T[thread_id = thread-alpha]
T --> S1[Saved StateSnapshot]
S1 --> R[Next invoke with same thread resumes state]
F[thread_id = thread-fresh] -. separate lineage .-> X[No old facts]
Code: src/checkpoints_vs_stores/checkpoint_demo.py
python -m checkpoints_vs_stores.demo storeOutput (from
artifacts/sample-output/store_demo.txt):
thread-a / user-ada:
Stored long-term memory: favorite_language=Python for user_id=user-ada.
thread-b / user-ada:
Your favorite language is Python. I found that in the Store, not this thread.
thread-c / user-grace:
I don't have a favorite language for this user in the Store.
thread-b is a brand-new thread, yet it recalls Python because the store
namespace (user-ada, "profile") is shared across threads. user-grace has a
different namespace, so she gets nothing.
flowchart LR
A[Thread A] --> G[Graph node]
G --> P[Store put]
P --> NS[(Namespace: user-ada/profile)]
NS --> K[favorite_language = Python]
B[Thread B] --> G2[Graph node]
G2 --> Q[Store get]
Q --> NS
C[Thread C, user-grace] -. different namespace .-> Empty[No memory]
Code: src/checkpoints_vs_stores/store_demo.py
python -m checkpoints_vs_stores.demo bothThe combined demo compiles one graph with both checkpointer=... and
store=..., the way production agents usually run, and shows that:
- checkpointed state stays separate per thread, and
- store memory is shared by namespace.
Full output:
artifacts/sample-output/combined_demo.txt.
Code: src/checkpoints_vs_stores/combined_demo.py
flowchart TB
subgraph ThreadScoped[Checkpoint layer]
CP[(Checkpointer)]
CP --> TH1[thread-alpha snapshots]
CP --> TH2[thread-beta snapshots]
end
subgraph CrossThread[Store layer]
ST[(Store)]
ST --> U1[user-ada/profile]
ST --> U2[user-grace/profile]
end
Graph[LangGraph app] --> CP
Graph --> ST
User[Runtime context: user_id] --> Graph
Config[Config: thread_id] --> Graph
Chapter 1's memory dies with the process (InMemorySaver/InMemoryStore).
Chapter 2 makes it survive — same graphs, real backends:
python -m checkpoints_vs_stores.chapter2 all # or: lg-memory-demo2 allFour demos, rendered in the same panel TUI:
- Kill-and-resume (
resume_demo.py) — process A learns your name and exits; process B (a different PID, verified) resumes the samethread_idfrom a SQLite file and answers. The demo in-memory savers can never do this. - Inside the database (
peek_demo.py) — one SQLite file holding both layers, read back with plain SQL: checkpoint rows are opaque msgpack snapshots owned by the framework; store rows are readable JSON you designed. - Semantic search (
search_demo.py) —store.search(namespace, query="what is the user's cat called?")ranks memories by meaning. Deterministic toy embeddings keep it offline; the index config and API are exactly what you'd use with a real model. - Backend matrix (
matrix_demo.py) — the same write-in-thread-a / recall-in-thread-b graph runs unchanged on all four backends viabackends.py:
| Backend | Works out of the box? | Reach for it when |
|---|---|---|
memory |
yes | dev and tests: zero setup, gone on restart |
sqlite |
yes | local apps and PoCs: one durable file, no server |
postgres |
docker compose up -d |
production default: one database for both layers |
redis |
docker compose up -d |
low latency, native TTLs (Redis 8+ bundles JSON + search) |
Postgres and Redis run from docker-compose.yml on
non-standard ports (5442, 6390) so they never clash with local servers; the
matrix reports them as unreachable (with the command to fix it) instead of
failing when they're down. Their tests auto-skip locally and run in CI
against real service containers. Override connection URLs with
LG_DEMO_POSTGRES_URL / LG_DEMO_REDIS_URL.
The two remaining canonical checkpoint patterns — the ones enterprises ask about first (see the use-cases guide):
python -m checkpoints_vs_stores.chapter3 all # or: lg-memory-demo3 all- Human-in-the-loop (
hitl_demo.py) — a refund agent callsinterrupt()before the irreversible step; the paused state lands in SQLite, and a rebuilt graph (fresh objects, same file — as after a restart) resumes it withCommand(resume={"approve": ...}). Both the approved and rejected paths run, and the demo counts node entries to prove the documented gotcha: resume re-executes the interrupted node from its start, so pre-interrupt side effects must be idempotent. - Time travel (
timetravel_demo.py) — after a conversation, the demo picks the checkpoint just before an answer and does both operations: replay (same reply reproduced) and fork (update_stateedits the past, the new branch answers differently). The thread's history keeps both timelines — an audit trail for free.
The demo modules carry demo scaffolding (result dicts, formatters), so
examples/ contains the same patterns as minimal standalone
scripts you can copy straight into a project:
| Recipe | The whole trick |
|---|---|
01_checkpointer_minimal.py |
compile(checkpointer=...), then pass {"configurable": {"thread_id": ...}} on invoke |
02_store_minimal.py |
compile(store=...), then read/write runtime.store inside a node |
03_both_minimal.py |
compile(checkpointer=..., store=...) — the production shape |
Each runs standalone (python examples/01_checkpointer_minimal.py), asserts
what it claims, and is executed by the test suite. The lines that matter:
graph = builder.compile(checkpointer=InMemorySaver(), store=InMemoryStore())
config = {"configurable": {"thread_id": "thread-1"}} # checkpoint scope
context = Context(user_id="user-ada") # store namespace scope
graph.invoke({"user_message": "hi"}, config, context=context)Swap InMemorySaver / InMemoryStore for persistent backends in production —
see docs/production-notes.md.
make artifacts (or python scripts/generate_artifacts.py) reruns all three
demos and rewrites:
artifacts/sample-output/— the outputs quoted aboveartifacts/demo-summary.json— machine-readable resultsartifacts/comparison-matrix.csv— the comparison table as data
Checkpoint ids and timestamps are redacted during generation, so the committed files are byte-stable across reruns.
.
├── .github/workflows/ci.yml # Pipeline: lint + tests + artifact rendering
├── artifacts/ # Generated demo evidence
├── diagrams/ # Mermaid diagram sources
├── docker-compose.yml # Chapter 2 backends: Postgres + Redis
├── docs/ # Concept notes, runbook, production notes
├── examples/ # Minimal copy-paste recipes
├── scripts/generate_artifacts.py # Rebuild text/JSON/CSV artifacts
├── src/checkpoints_vs_stores/ # The LangGraph demos
└── tests/ # Pytest coverage for the behavior
GitHub Actions runs four jobs on every push and pull request: lint (ruff
check + format), tests (pytest on Python 3.10 and 3.13),
backend-integration (chapter 2 tests against real Postgres and Redis
service containers), and demo-artifacts (reruns the demos and uploads
artifacts/).
- Persistence overview: https://docs.langchain.com/oss/python/langgraph/persistence
- Checkpointers: https://docs.langchain.com/oss/python/langgraph/checkpointers
- Stores: https://docs.langchain.com/oss/python/langgraph/stores
- Memory guide: https://docs.langchain.com/oss/python/langgraph/add-memory
- PyPI package: https://pypi.org/project/langgraph/