Appearance
5.3 — Checkpointing and Persistence
1. What is it?
Checkpointing is LangGraph's mechanism for automatically persisting a graph's state after every superstep (Part 5.1), to a durable store. This is what makes a graph's execution resumable — after a process crash, a deliberate pause (Part 5.4's human-in-the-loop interrupts), or simply a long time gap between steps, execution can continue from exactly where it left off, rather than restarting from scratch or being lost entirely.
2. Why does it exist?
Part 3.7's agent loops and Part 5.1's graph state, as described so far, exist only in memory during a single process's execution — if that process crashes mid-run, or if a workflow genuinely needs to pause for hours or days (waiting on a human's approval, Part 5.4, or an external event), all progress is lost unless something explicitly persists the state along the way. Checkpointing exists to solve this without requiring you to hand-write state-serialization and resumption logic for every workflow — it's a structural, built-in property of the graph execution model, not a feature you build on top of it.
3. What problem does it solve?
It solves durability and resumability for multi-step LLM workflows — a genuinely different and harder problem than a single stateless API call's reliability (Part 1.2's idempotency discussion), because a multi-step graph has accumulated, in-progress state that would be expensive or impossible to simply recompute from scratch after an interruption (re-running three already-completed LLM calls in a loan review, for instance, wastes cost and, if any of those calls have external side effects like sending a notification, could cause them to happen twice).
4. How does it work internally?
Checkpointers and the persistence layer
A checkpointer is attached at compile() time (Part 5.1) and is responsible for saving the graph's full state after each superstep, associated with a specific thread ID (an identifier for one specific, ongoing conversation/execution — analogous to Part 3.9's session concept):
python
from langgraph.checkpoint.memory import MemorySaver
compiled = graph.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "loan-application-4521"}}
result = await compiled.ainvoke(initial_state, config=config)Three checkpointer implementations, corresponding to genuinely different durability guarantees:
MemorySaver: keeps checkpoints in the running process's memory only. Fast, zero setup, but not durable — a process restart loses everything. Appropriate for local development and testing only, never for anything requiring real resumability across a restart.SqliteSaver: persists checkpoints to a local SQLite file. Durable across process restarts on a single machine, but not suitable for a distributed, multi-instance production deployment (Part 7's horizontal scaling) since it's tied to one machine's local filesystem.PostgresSaver: persists checkpoints to a Postgres database (Part 1.5) — the current recommended choice for production, since it provides genuine durability that survives process restarts and works correctly across multiple application instances (Part 7's horizontal scaling), because all instances share the same underlying database rather than each having its own isolated local state.
Verify the exact current import paths and constructor signatures for these checkpointer classes against current LangGraph documentation before shipping — this is exactly the kind of API surface that's actively maintained and can shift between versions.
A fourth tier: Redis-backed checkpointing
Beyond the three built-in tiers above, LangGraph also supports a Redis-backed checkpointer (via a separate, community/ecosystem package — verify the current package name and API against current LangGraph documentation, since this surface is less centrally maintained than the core checkpointer implementations) for teams already running Redis (Part 1.6) as production infrastructure. The tradeoff profile is genuinely distinct from both SqliteSaver and PostgresSaver, not simply "a faster Postgres":
- Fast and well-suited to short-lived or high-throughput threads — Redis's in-memory-first design gives it materially lower write latency per checkpoint than a relational write to Postgres, which matters for graphs with many small supersteps (Part 5.1, section 11's node-granularity/checkpoint-latency trade-off) or high-volume, short-duration conversational threads that don't need to survive for days.
- Weaker durability guarantees than Postgres by default — Redis is fundamentally an in-memory store; unless it's explicitly configured with AOF (append-only file) persistence or an equivalent durability setting, a Redis restart can lose recent writes in a way a properly configured Postgres instance's WAL-backed durability does not. Using Redis as a checkpoint store for a workflow that must survive days-long human-in-the-loop pauses (Part 5.3, section 6's loan-officer example) without confirming AOF/persistence is configured reproduces exactly the
MemorySaver-in-production risk this chapter warns against, just with a longer (but still finite, and easy to overlook) time window before data loss. - No native relational query over checkpoint history — Postgres's relational structure lets you write ad hoc SQL against checkpoint history for debugging or support (e.g., "find every paused thread older than 48 hours," directly useful for section 20's FDE scenario); Redis's key-value model doesn't offer this natively, so auditing or bulk-querying checkpoint state across many threads typically requires either scanning keys by convention or maintaining a separate index.
In practice, Redis-backed checkpointing is a reasonable choice for high-throughput, short-lived thread workloads where the team already operates Redis and has explicitly configured it for the durability level the workload actually requires — not a default drop-in replacement for PostgresSaver in the general case.
What resuming actually looks like
python
# Some time later — possibly after a full process restart, or an interrupt (Part 5.4)
config = {"configurable": {"thread_id": "loan-application-4521"}}
result = await compiled.ainvoke(None, config=config) # None input resumes from last checkpointPassing None (or the framework's specific resume-signal, verified against current docs) as input with the same thread_id tells LangGraph to load the last checkpoint for that thread and continue execution from there, rather than starting a fresh run — the graph doesn't re-execute nodes that already completed and were checkpointed; it resumes at the point execution was interrupted.
The relationship to human-in-the-loop (a preview of Part 5.4)
Checkpointing is the load-bearing infrastructure underneath LangGraph's interrupt() mechanism (Part 5.4): pausing a graph run for human approval only works reliably in production because the state at the pause point is genuinely persisted (via PostgresSaver or equivalent) — if you paused a run using only MemorySaver and the process restarted before a human approved it, that paused run and all its accumulated state would simply be gone. This is precisely why Part 5.4's human-in-the-loop pattern is described there as depending on a persistent checkpointer — it's not a separate feature, it's checkpointing applied to a specific, high-value use case.
Durability modes — a latency/risk-window knob, not an unconditional "always after every superstep"
The description above (and section 1's framing) simplifies slightly by saying checkpoints happen "after every superstep" as if that were the only option. LangGraph actually exposes a durability configuration controlling when, relative to a node's execution, a checkpoint write actually happens — verify the exact current parameter name and accepted values (e.g., a durability setting distinguishing something like a synchronous write before/after each node executes versus deferring persistence until the graph exits or is interrupted) against current LangGraph documentation, since this is exactly the kind of tunable that gets renamed or restructured across versions. The tradeoff it controls is straightforward: a mode that persists more eagerly (synchronously, before or immediately after each node) gives the strongest crash-recovery guarantee — the smallest possible window of completed-but-unpersisted work that a crash could lose — at the cost of a real, added write-latency tax on every single superstep (section 11's checkpoint-latency discussion). A less eager mode (persisting less frequently, e.g., only at graph exit or interrupt) reduces that per-step latency cost but widens the risk window: a crash between the last persisted point and the moment of failure loses correspondingly more in-progress work. Choose eager durability for high-stakes, side-effecting workflows (this chapter's loan-review example) where losing even one completed-but-unpersisted step is unacceptable, and a less eager mode for high-throughput, lower-stakes graphs where per-step write latency matters more than a small window of potential replay on crash.
Thread-scoped checkpoints vs. cross-thread long-term memory (Store) — two different mechanisms, not one
Everything in this chapter concerns thread-scoped persistence: a checkpoint is keyed to one thread_id and captures that one conversation/execution's accumulated state, resumable only within that same thread. This is easy to conflate with "LangGraph's memory system" as if it were the whole story, but LangGraph provides a genuinely separate mechanism — commonly exposed as a Store — specifically for memory that needs to persist across threads/sessions (e.g., a fact learned about a user in one conversation that should be available in an entirely different, later conversation with the same user). Checkpointing answers "how does this specific run survive an interruption"; a cross-thread store answers "what does the system remember about this user (or entity) the next time a new thread starts" — structurally the same distinction Part 3.9 draws between working memory (a single conversation) and long-term memory (across sessions), with LangGraph's checkpointer implementing the former and its Store mechanism implementing (one architectural option for) the latter. See Part 3.9 for the broader memory-architecture discussion (semantic/structured/episodic long-term memory) that a Store-backed implementation sits underneath; verify the current Store API surface against current LangGraph documentation, as this is a comparatively newer part of the framework relative to the core checkpointer classes.
5. Simple mental model
Checkpointing is like autosave in a long, complex video game, keyed to a specific saved-game slot — MemorySaver is like a game that only keeps its state in RAM with no save file at all (turn off the console, and you've lost everything since you started); SqliteSaver is like saving to a local file on this specific console (durable if you turn the console off and back on, but useless if you wanted to continue on a different console); PostgresSaver is like a cloud save that any console, anywhere, can load and continue from — which is exactly the property a real production system running on multiple, interchangeable server instances needs.
6. Real-world example
A loan-application review graph (Part 5.1/5.2) using PostgresSaver completes the automated credit/income/fraud checks (three supersteps, each checkpointed), then hits a human-in-the-loop interrupt (Part 5.4) requiring a loan officer's sign-off on a borderline case. The loan officer doesn't act for two business days. Because the checkpoint is durably persisted in Postgres — not held in any single process's memory — the application can be redeployed, restarted, or scaled to different instances entirely during those two days with zero risk of losing the loan application's accumulated review state; when the loan officer's approval finally arrives (potentially routed to a completely different running instance than the one that originally paused), resuming from thread_id="loan-application-4521" picks up exactly where the process left off.
7. Architecture diagram
Graph executionsuperstep 1 → checkpoint saved → superstep 2 → checkpoint saved → superstep 3 → interrupt (Part 5.4) → checkpoint saved (paused state)
PostgresSaverproduction: durable, survives restarts, shared across instances (Part 1.5)
New/same process resumesloads checkpoint for thread_id, continues from exact pause point — possibly days later
8. Production considerations
- Use
PostgresSaver(or an equivalent durable, shared-storage checkpointer) for any production deployment —MemorySaverin production is a data-loss incident waiting to happen the moment a process restarts (a deploy, a crash, an autoscaling event), andSqliteSaverdoesn't work correctly across the multiple instances a horizontally-scaled deployment (Part 7) requires. - Design
thread_idscheme deliberately — it's the key that ties a specific execution to its checkpoint history; a well-designed scheme (e.g., incorporating a tenant ID and a business entity ID, like the loan application ID in section 6) makes checkpoints easy to look up and reason about for debugging and support. - Checkpoint storage grows over time and needs a retention policy — exactly Part 3.9's memory-retention discussion applied to graph checkpoints: decide explicitly how long completed threads' checkpoint history should be retained, rather than accumulating it indefinitely.
- Checkpointed state is exactly as sensitive as the data it contains (Part 5.1, section 10's warning restated) — a Postgres-backed checkpoint store holding loan applications' financial data needs the same access-control, encryption, and compliance treatment as any other sensitive data store (Part 1.5, Part 9.6).
- Verify checkpoint-store integrity/provenance before restoring untrusted state into a live session — if a checkpoint store is ever reachable by a less-trusted party than the application itself (a shared instance, a multi-tenant store without the scoping section 6/8 already require), restoring a tampered checkpoint is an insecure-deserialization risk, not just a data-integrity one (Part 9.6's dedicated treatment).
- If choosing a Redis-backed checkpointer over Postgres, confirm AOF/persistence is explicitly configured before treating it as durable enough for anything that must survive a multi-hour or multi-day pause (section 4's Redis subsection) — an unconfigured, default Redis instance is a durability regression relative to
PostgresSaver, not a straightforward performance upgrade. - Choose a durability mode deliberately, per graph (section 4) rather than accepting an unexamined default — a high-stakes, side-effecting workflow warrants the most eager (lowest-risk-window) mode even at some per-step latency cost; a high-throughput, lower-stakes graph may reasonably trade some risk window for lower latency.
- Don't reach for checkpointing to solve a cross-thread memory need — if the requirement is "remember this about the user in their next conversation," that's Part 3.9's long-term memory problem, addressed via a
Store-style mechanism (section 4), not by trying to extend thread-scoped checkpoint retention across threads it was never designed to span.
9. Common mistakes
- Deploying to production with
MemorySaver(perhaps left over from local development) and discovering data loss only after the first production restart or deploy. - Using
SqliteSaverin a horizontally-scaled, multi-instance deployment, causing inconsistent or missing state depending on which instance happens to handle a given resume request. - Not having a retention/cleanup policy for checkpoint storage, leading to unbounded growth over time.
- Treating
thread_idcasually (e.g., a random UUID with no meaningful structure) making it hard to look up or debug a specific customer's in-progress workflow during a support incident. - Adopting a Redis-backed checkpointer for its latency benefit without confirming AOF/persistence is configured, effectively reintroducing a
MemorySaver-like durability gap under a different, more "production-sounding" name. - Conflating thread-scoped checkpointing with LangGraph's entire memory story, and being surprised that a fact from one thread isn't available in a different thread — that's what the separate
Store/long-term-memory mechanism (section 4, Part 3.9) is for.
10. Security considerations
- Checkpoint storage is a genuine, durable data store containing potentially sensitive accumulated workflow state — subject to the same tenant-isolation, access-control, and encryption-at-rest considerations as any other production database (Part 1.5, Part 9.6), and specifically worth auditing since it's easy to overlook "the checkpoint database" as a security-relevant asset compared to the more obviously named "customer database."
- A
thread_idthat's guessable or enumerable could allow an unauthorized party to resume or inspect another user's in-progress workflow if access control isn't enforced at the application layer wrapping the resume call — the checkpointer itself doesn't enforce authorization; your application code must verify the requester is authorized for the specificthread_idbefore resuming.
11. Performance considerations
- Checkpointing after every superstep adds a real, measurable write to durable storage on every step — for very high-frequency, fine-grained graphs (many small supersteps), this can become a meaningful latency/throughput factor, worth profiling against your specific checkpointer and node granularity (Part 5.1's node-granularity trade-off discussion applies directly here).
- Large state objects (Part 5.1, section 11) compound this cost, since more data must be serialized and written on every checkpoint.
- The durability mode (section 4) is a direct lever on this cost: a more eager, synchronous-write mode adds more per-step latency than a mode that defers persistence to graph exit — measure both the latency cost and the crash risk window before choosing, rather than optimizing one in isolation.
- Redis's lower write latency relative to Postgres (section 4) is precisely why it suits high-frequency, fine-grained graphs whose per-step checkpoint cost would otherwise dominate — but only once its durability configuration matches the workload's actual requirements.
12. Cost considerations
PostgresSaver's ongoing storage and write cost scales with checkpoint frequency, state size, and thread retention duration — a real, budgetable infrastructure cost, distinct from LLM token cost, worth accounting for explicitly in a production system's cost model (Part 7.10).
13. When to use it
Any production graph, without exception, should use a durable checkpointer (PostgresSaver or equivalent) — the only legitimate use for MemorySaver is local development/testing where losing state on restart is acceptable and expected.
14. When NOT to use it
A genuinely single-step, stateless graph with no need for resumability might not benefit meaningfully from checkpointing overhead — though in practice, most graphs complex enough to need LangGraph at all (Part 5.1's "when to use it") also benefit from at least the debugging/observability value of checkpoint history, even if resumability itself isn't strictly required.
15. Alternatives and trade-offs
| Checkpointer | Good for | Weak point |
|---|---|---|
MemorySaver | Local development, testing, zero setup | Not durable — data loss on any process restart |
SqliteSaver | Single-machine durability, simple local deployments | Doesn't work correctly across multiple instances |
PostgresSaver | Production, durable, multi-instance-safe | Requires operating a Postgres instance (Part 1.5) — real infrastructure |
| Redis-backed checkpointer | High-throughput or short-lived threads needing low write latency | Weaker durability than Postgres unless AOF/persistence is explicitly configured; no native relational query over checkpoint history |
16. Practical Python/code example
python
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
# Verify exact current import path and setup/connection API against current
# LangGraph documentation — checkpointer packages are an actively maintained
# part of the API surface.
async def build_production_graph(db_connection_string: str):
"""
Builds and compiles the support workflow graph with a durable, production-grade
Postgres checkpointer.
Args:
db_connection_string (str): Connection string for the checkpoint database.
Returns:
A compiled graph with persistence configured for production use.
"""
checkpointer = AsyncPostgresSaver.from_conn_string(db_connection_string)
await checkpointer.setup() # creates required tables if not already present
graph = StateGraph(SupportState)
graph.add_node("classify", classify_node)
graph.add_node("respond", respond_node)
graph.set_entry_point("classify")
graph.add_edge("classify", "respond")
graph.add_edge("respond", END)
return graph.compile(checkpointer=checkpointer)17. Production-quality example
A resume-with-authorization wrapper, directly addressing the section 10 concern that checkpointers don't enforce authorization themselves:
python
import logging
logger = logging.getLogger("graph_resume")
class AuthorizedGraphResumer:
"""Wraps graph resumption with an explicit authorization check, since the
checkpointer itself has no concept of which user is allowed to resume which thread."""
def __init__(self, compiled_graph, thread_ownership_lookup):
"""
Args:
compiled_graph: A compiled LangGraph graph with a durable checkpointer.
thread_ownership_lookup: An async callable(thread_id) -> owning_user_id,
backed by your application's own authoritative record of thread ownership.
"""
self._graph = compiled_graph
self._thread_ownership_lookup = thread_ownership_lookup
async def resume(self, thread_id: str, current_user_id: str) -> dict:
"""
Resumes a paused thread only if the current user is verified to own it.
Args:
thread_id (str): The thread to resume.
current_user_id (str): The authenticated user requesting the resume.
Returns:
dict: The graph's result after resuming.
Raises:
PermissionError: If the current user doesn't own this thread.
"""
owning_user_id = await self._thread_ownership_lookup(thread_id)
if owning_user_id != current_user_id:
logger.warning(
"user=%s denied resume of thread=%s (owned by %s)",
current_user_id, thread_id, owning_user_id,
)
raise PermissionError("not authorized to resume this thread")
config = {"configurable": {"thread_id": thread_id}}
return await self._graph.ainvoke(None, config=config)18. Short exercise
A production incident report notes that a loan-review workflow "lost" three in-progress applications during a routine deployment. Given this chapter's content, list the specific checkpointer-related questions you'd ask to diagnose the root cause, and state the most likely misconfiguration.
19. Interview questions
- Explain the durability difference between
MemorySaver,SqliteSaver, andPostgresSaver, and why only one of them is appropriate for a horizontally-scaled production deployment. - Why does human-in-the-loop pausing (Part 5.4) fundamentally depend on a persistent checkpointer rather than being a separate, independent feature?
- What authorization gap exists in resuming a graph by
thread_idalone, and how would you close it? - What tradeoff does a Redis-backed checkpointer make relative to
PostgresSaver, and what has to be true of the Redis deployment for it to be a reasonable production choice? - What does a checkpointing durability mode actually control, and what's the tradeoff between its more-eager and less-eager settings?
- What's the difference between thread-scoped checkpointing and LangGraph's cross-thread
Store/long-term-memory mechanism, and why would conflating them cause a bug?
20. FDE/customer scenario
Customer's engineering team: "Our multi-step AI workflow occasionally loses progress when we deploy a new version — is that expected?"
This maps almost exactly onto section 6/9's failure mode: check whether the deployment is using a durable, shared checkpointer (PostgresSaver) or an in-memory/single-instance one (MemorySaver/SqliteSaver) that can't survive a redeploy or doesn't work correctly across the multiple instances a rolling deployment briefly runs. This is a concrete, specific, and often quick fix once correctly diagnosed — exactly the kind of grounded technical judgment call that builds credibility in a support/debugging engagement.
Key takeaways
- Checkpointing persists graph state keyed by
thread_id, making multi-step workflows resumable across interruptions; a durability mode controls exactly when those writes happen, trading per-step latency against crash risk window. MemorySaveris development-only;SqliteSaverworks for single-machine durability;PostgresSaveris the production standard for durable, multi-instance-safe persistence; a Redis-backed checkpointer trades some of that durability (unless AOF/persistence is explicitly configured) for lower write latency on high-throughput or short-lived threads.- Human-in-the-loop pausing (Part 5.4) fundamentally depends on a persistent checkpointer — it's an application of checkpointing, not a separate mechanism.
- Thread-scoped checkpointing is not the same mechanism as LangGraph's cross-thread
Store/long-term-memory system (Part 3.9) — one resumes a specific run, the other recalls information across different runs/sessions.
Things you should be able to explain
- The durability differences between the checkpointer types (including Redis) and why only
PostgresSaver/a properly configured Redis suit horizontally-scaled production. - Why checkpoint storage itself needs the same security/retention treatment as any other sensitive data store.
- What a durability mode controls and the tradeoff between an eager and a deferred write policy.
- Why thread-scoped checkpoints and cross-thread long-term memory are different mechanisms solving different problems.
Things you should be able to build
- A production graph compiled with a durable Postgres checkpointer, and an authorization wrapper around thread resumption.
Common mistakes
- Deploying with
MemorySaverin production. - Using
SqliteSaverin a multi-instance deployment. - No authorization check on who can resume a given thread.
- Using Redis as a checkpoint store without confirming AOF/persistence is configured.
- Trying to use thread-scoped checkpoints to solve a cross-thread, long-term memory need.
Recommended next chapter
04-interrupts-human-in-the-loop.md