Skip to content

sw30labs/langgraph-checkpoints-vs-stores

Repository files navigation

LangGraph Checkpoints vs Stores

CI Python 3.10+ LangGraph ≥1.2 License: Apache 2.0 Code style: Ruff Tested with pytest Runs offline

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.

The difference in one screen

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.

Which one do I need?

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=...)"]
Loading

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.

Quickstart

One command — creates .venv, installs, lints, tests, and runs every demo:

./setup_and_run.sh

Or 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
pytest

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

Demo 1: checkpoints are scoped to a thread

python -m checkpoints_vs_stores.demo checkpoint

Output (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]
Loading

Code: src/checkpoints_vs_stores/checkpoint_demo.py

Demo 2: stores share memory across threads

python -m checkpoints_vs_stores.demo store

Output (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]
Loading

Code: src/checkpoints_vs_stores/store_demo.py

Demo 3: both layers together

python -m checkpoints_vs_stores.demo both

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

Chapter 2: production backends

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 all

Four 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 same thread_id from 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 via backends.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.

Chapter 3: human-in-the-loop and time travel

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 calls interrupt() before the irreversible step; the paused state lands in SQLite, and a rebuilt graph (fresh objects, same file — as after a restart) resumes it with Command(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_state edits the past, the new branch answers differently). The thread's history keeps both timelines — an audit trail for free.

Use it in your own code

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.

Generated artifacts

make artifacts (or python scripts/generate_artifacts.py) reruns all three demos and rewrites:

Checkpoint ids and timestamps are redacted during generation, so the committed files are byte-stable across reruns.

Repo layout

.
├── .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

CI

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/).

References

License

Apache 2.0

About

Runnable, deterministic LangGraph examples: checkpoints (thread-scoped state) vs stores (cross-thread memory). TUI demos, copy-paste recipes, researched use-cases guide. No LLM calls, no API keys.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors