new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

LangChain · LangGraph · all subjects

persistence/checkpointing

103 notes in this subject, read out of this brain and free to use. This is page 1 of 2.

LangGraph applies latest graph to all threads including those resuming from checkpoints

LangGraph runs the latest deployed graph against state that has been persisted for existing threads. Unlike workflow engines that pin a run to the version of code it started with, LangGraph applies the latest graph immediately to every thread, both new threads and threads that resume from a checkpoint. This means bug fixes propagate to in-flight conversations and agents without ceremony, but you must reason about how each change interacts with runs that started under the previous version of the code.

Three categories of backward compatibility issues in LangGraph

There are three categories of compatibility issues to watch for when updating graph code: (1) Technical compatibility - the most common; the new code must still load and execute against existing State. (2) Business compatibility - less common; existing runs should keep following the old business logic even though the code has changed. (3) Non-determinism - only applies to the Functional API.

Technical compatibility: API contract between graph code and persisted state

Technical compatibility is the equivalent of an API breaking change in a microservice. The 'API' is the contract between your graph code and the data already persisted by the checkpointer for existing threads. When a thread resumes, LangGraph deserializes the saved state, dispatches it to a node by name, and expects the node to return values that fit the state schema.

Common technical compatibility breakages in LangGraph

Common technical breakages include: (1) Renaming or removing a node while threads are paused at or about to enter that node, for example at an interrupt or via a checkpointed conditional edge that still routes to the old name. On resume, LangGraph cannot find the node by its saved name and the run fails. (2) Renaming or removing a State key that older checkpoints still contain or that downstream nodes still read. (3) Tightening a State field, such as making an Optional field required, narrowing a type, or adding a new required field with no default.

Pattern: Rename state fields or nodes via add-then-remove

Rename through add-then-remove. Add the new field or node alongside the old one, dual-write or route to both for a deprecation window, then remove the old one once you have confirmed no in-flight thread depends on it.

Use time travel and graph.get_state to validate backward compatibility

Use time travel and graph.get_state to spot-check existing threads against the new code in a staging deployment before rolling out. This helps validate backward compatibility of changes.

Detecting in-flight threads: LangSmith Agent Server thread search

If you deploy to LangSmith, use the Agent Server's thread search to filter by status. The status field accepts idle, busy, interrupted, and error, so you can bulk-query for interrupted or busy threads, optionally narrowed with metadata filters. This helps identify if any threads are currently parked on the version of code you are about to drop.

Detecting in-flight threads: graph.get_state and graph.get_state_history

When you already have a thread_id, use graph.get_state(config) to return the latest checkpoint, including which node the thread is paused at and any pending interrupts. Use graph.get_state_history(config) to return the full chronological list of checkpoints for the thread.

Business compatibility: Versioning behavior with flow_version

When a change is technically valid but the meaning of the new graph differs from the old one, use a behavioral version pattern. Record the relevant behavioral version on the state at thread start, then branch on it with a conditional edge. Old threads that resume read the flow_version from their saved state (or fall through to the v1 default) and skip the new steps. New threads start at the beginning, are stamped with the new version number, and run the new path.

Business compatibility: Set version at thread start before any branch

The flow_version pattern only works if you set the version at thread start, before any branch that needs to be versioned. Setting it later means existing threads will not have it set when they need it.

Non-determinism only applies to Functional API and interrupt/task calls

Non-determinism is a backward compatibility concern only for the Functional API and for tasks or interrupt calls inside a Graph API node. Plain Graph API nodes re-run from the start of the node function on resume; design side effects to be idempotent, but you do not need to preserve task call order unless you use tasks or interrupt in that node.

Functional API non-determinism: Adding, removing, or reordering task/interrupt calls

A Functional API entrypoint compiles to a single node that replays the entrypoint body from the beginning when a run resumes, using cached task results to skip work that has already been done. Adding, removing, or reordering task calls or interrupt calls that come before the resume point breaks this model because LangGraph matches cached results and resume values to calls by their position in the replay, so shifting that position can cause the wrong cached value to be replayed against a different call.

Functional API non-determinism: Non-deterministic operations outside tasks

Introducing non-deterministic operations outside of a task in a Functional API entrypoint, such as time.time(), random.random(), or a network call inlined in the entrypoint body, breaks the replay model. On replay these produce different values than they did on the first run, which can change the control flow.

Functional API: Options for deploying non-trivial code changes to in-flight runs

If you need to make non-trivial code changes to an entrypoint that has in-flight runs, the safest options are: (1) Let in-flight runs drain before deploying the change. (2) Wrap any new logic in a new task so its results are checkpointed independently. (3) Register a new entrypoint under a new graph name in langgraph.json for the new behavior, and route new threads to it.

Example: Business compatibility with flow_version pattern

from typing import NotRequired from typing_extensions import TypedDict from langgraph.graph import END, START, StateGraph class State(TypedDict): request: str flow_version: NotRequired[int] response: NotRequired[str] def intake(state: State) -> dict: # Stamp new threads with the current flow version. Existing threads # that resume past `intake` keep whatever value was already saved. return {"flow_version": state.get("flow_version", 2)} def triage(state: State) -> dict: ... def policy_check(state: State) -> dict: ... def respond(state: State) -> dict: ... def after_triage(state: State) -> str: if state.get("flow_version", 1) >= 2: return "policy_check" return "respond" builder = StateGraph(State) builder.add_node("intake", intake) builder.add_node("triage", triage) builder.add_node("policy_check", policy_check) builder.add_node("respond", respond) builder.add_edge(START, "intake") builder.add_edge("intake", "triage") builder.add_conditional_edges("triage", after_triage, ["policy_check", "respond"]) builder.add_edge("policy_check", "respond") builder.add_edge("respond", END) graph = builder.compile() This example shows how to handle the case where you insert a new policy_check step between triage and respond. Old threads that resume after triage read flow_version from their saved state (or default to v1) and skip policy_check. New threads are stamped with flow_version=2 at intake and run the new path.

What are checkpointers in LangGraph

A checkpointer saves a snapshot of graph state at each super-step, organized into threads. Checkpointers enable human-in-the-loop workflows, time travel debugging, fault-tolerant execution, and conversational memory.

Thread ID requirement for checkpointers

When invoking a graph with a checkpointer, you must specify a thread_id as part of the configurable portion of the config. The checkpointer uses thread_id as the primary key for storing and retrieving checkpoints. Without it, the checkpointer cannot save state or resume execution after an interrupt.

What is a super-step in LangGraph

A super-step is a single tick of the graph where all nodes scheduled for that step execute potentially in parallel. LangGraph creates a checkpoint at each super-step boundary. For a sequential graph like START -> A -> B -> END, there are separate super-steps for the input, node A, and node B, producing a checkpoint after each one. Understanding super-step boundaries is important for time travel because you can only resume execution from a checkpoint at a super-step boundary.

Pending writes in LangGraph checkpointing

When a graph node fails mid-execution at a given super-step, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that super-step. These per-task writes enable pending writes recovery: if another node in the same super-step fails, the successful nodes' writes are already durable and do not need to be re-run on resume.

Checkpoint namespace for parent and subgraphs

Each checkpoint has a checkpoint_ns (checkpoint namespace) field that identifies which graph or subgraph it belongs to. An empty string "" means the checkpoint belongs to the parent (root) graph. "node_name:uuid" means the checkpoint belongs to a subgraph invoked as the given node. For nested subgraphs, namespaces are joined with | separators (e.g., "outer_node:uuid|inner_node:uuid").

StateSnapshot object fields

StateSnapshot has the following fields: values (dict of state channel values at checkpoint), next (tuple/list of node names to execute next, empty means graph is complete), config (contains thread_id, checkpoint_ns, checkpoint_id), metadata (dict with source="input"/"loop"/"update", writes showing node outputs, and step counter), created_at (ISO 8601 timestamp), parent_config (config of previous checkpoint or None for first), and tasks (tuple/list of PregelTask objects with id, name, error, interrupts, and optionally state for subgraph snapshots).

Get current graph state with get_state

Call graph.get_state(config) to view the latest state of the graph. This returns a StateSnapshot object corresponding to the latest checkpoint associated with the thread ID provided in the config. You can also provide a specific checkpoint_id in the config to get a state snapshot for that checkpoint: {"configurable": {"thread_id": "1", "checkpoint_id": "1ef663ba-28fe-6528-8002-5a559208592c"}}

Get full state history with get_state_history

Call graph.get_state_history(config) to get the full history of graph execution for a given thread. This returns a list of StateSnapshot objects associated with the thread ID. Checkpoints are ordered chronologically with the most recent checkpoint being first in the list.

Filter state history to find specific checkpoints

You can filter the state history returned by get_state_history to find checkpoints matching criteria. Examples: find checkpoint before a specific node with next==("node_name",), find by step number with metadata["step"]==N, find checkpoints created by update_state with metadata["source"]=="update", find where interrupt occurred by checking if tasks have non-empty interrupts.

Replay past executions from a checkpoint

Replay re-executes steps from a prior checkpoint. Invoke the graph with a prior checkpoint_id to re-run nodes after that checkpoint. Nodes before the checkpoint are skipped (their results are already saved). Nodes after the checkpoint re-execute, including any LLM calls, API requests, or interrupts which are always re-triggered during replay.

Update graph state with update_state

You can edit the graph state using update_state (Python) or updateState (JavaScript). This creates a new checkpoint with the updated values without modifying the original checkpoint. The update is treated the same as a node update: values are passed through reducer functions when defined, so channels with reducers accumulate values rather than overwrite them. You can optionally specify as_node to control which node the update is treated as coming from, affecting which node executes next.

Three durability modes for checkpoints

LangGraph supports three durability modes balancing performance and data consistency. "exit" persists changes only when graph execution exits, providing best performance for long-running graphs but intermediate state is not saved. "async" persists changes asynchronously while next step executes, providing good performance and durability with small risk of lost checkpoints on process crash. "sync" persists changes synchronously before next step starts, ensuring every checkpoint is written before continuing, providing high durability at performance cost.

DeltaChannel to optimize checkpoint storage

DeltaChannel stores only incremental deltas instead of full accumulated values, substantially reducing checkpoint size for append-heavy channels like multi-turn conversation messages. This makes checkpoint storage O(1) per step instead of O(N) for channels that accumulate over time. DeltaChannel requires langgraph>=1.2 and is currently in beta, so the API may change in future releases.

BaseCheckpointSaver interface methods

Each checkpointer implements the BaseCheckpointSaver interface with these methods: .put (sync) / .aput (async) - store a checkpoint with configuration and metadata; .put_writes / .aput_writes - store intermediate writes linked to a checkpoint; .get_tuple / .aget_tuple - fetch a checkpoint tuple for a given configuration (thread_id and checkpoint_id); .list / .alist - list checkpoints matching configuration and filter criteria; .delete_thread / .adelete_thread - delete all checkpoints and writes for a thread.

Checkpointer libraries available

LangGraph provides several checkpointer implementations: langgraph-checkpoint (base interface BaseCheckpointSaver and InMemorySaver for experimentation, included with LangGraph); langgraph-checkpoint-sqlite (SqliteSaver/AsyncSqliteSaver for SQLite, ideal for experimentation and local workflows); langgraph-checkpoint-postgres (PostgresSaver/AsyncPostgresSaver for Postgres, used in LangSmith, ideal for production); langchain-azure-cosmosdb (CosmosDBSaverSync/CosmosDBSaver for Azure Cosmos DB, ideal for production with Azure). JavaScript equivalents exist for most: @langchain/langgraph-checkpoint-sqlite (SqliteSaver), @langchain/langgraph-checkpoint-postgres (PostgresSaver), @langchain/langgraph-checkpoint-mongodb (MongoDBSaver with vector search support), @langchain/langgraph-checkpoint-redis (RedisSaver).

Serialization with JsonPlusSerializer

The default serializer, JsonPlusSerializer, uses ormsgpack and JSON to handle a wide variety of types including LangChain and LangGraph primitives, datetimes, enums, Pydantic v2 models, dataclasses, and numpy arrays. To add fallback to pickle for unsupported types (such as Pandas dataframes), use JsonPlusSerializer(pickle_fallback=True) when creating the checkpointer.

Encrypted checkpointer serialization

Checkpointers can optionally encrypt persisted state by passing an EncryptedSerializer instance to the serde argument of any BaseCheckpointSaver implementation. The easiest way to create an encrypted serializer is via EncryptedSerializer.from_pycryptodome_aes(), which reads the AES key from the LANGGRAPH_AES_KEY environment variable or accepts a key argument. When running on LangSmith, encryption is automatically enabled when LANGGRAPH_AES_KEY is present.

Checkpointer put method signature and requirements

The put/aput method stores one checkpoint row and must: serialize the checkpoint using self.serde.dumps_typed(checkpoint) to handle LangGraph-native types including _DeltaSnapshot blobs used by delta channels; store metadata in full without stripping unknown keys since LangGraph adds new metadata fields in minor releases; store config["configurable"].get("checkpoint_id") as the parent checkpoint ID so get_tuple can populate parent_config. Returns an updated config with the stored checkpoint_id.

Checkpointer get_tuple specific-id lookup criticality

When implementing get_tuple/aget_tuple, both paths must work: loading without checkpoint_id should return the latest checkpoint, and loading with a specific checkpoint_id should return that exact checkpoint. The specific-id path is used for time travel and critically for delta channel state reconstruction on every graph invocation. A broken specific-id lookup silently corrupts delta channel state.

Delta channel dependency on write history

DeltaChannel state is not self-contained in a single checkpoint—it depends on the ancestor write chain back to the nearest _DeltaSnapshot. When implementing prune or delete_for_runs with delta channels, you must not delete write rows that a surviving checkpoint's delta channels depend on. Safe options: walk ancestor chain before pruning and mark non-deletable writes, force a snapshot before pruning then delete ancestors freely, or skip pruning for delta-channel threads.

Checkpointer features requiring checkpoints

Checkpointers are required for the following features: Human-in-the-loop (inspect, interrupt, approve graph steps and resume after state updates); Memory (between interactions like conversations, allowing follow-up messages on the same thread); Time travel (replay prior executions to review/debug steps and fork state at arbitrary checkpoints); Fault-tolerance (restart from last successful step if nodes fail); Pending writes (resume without re-running successful nodes in same super-step).

Example checkpointer with InMemorySaver

from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import InMemorySaver from langchain_core.runnables import RunnableConfig from typing import Annotated from typing_extensions import TypedDict from operator import add class State(TypedDict): foo: str bar: Annotated[list[str], add] def node_a(state: State): return {"foo": "a", "bar": ["a"]} def node_b(state: State): return {"foo": "b", "bar": ["b"]} workflow = StateGraph(State) workflow.add_node(node_a) workflow.add_node(node_b) workflow.add_edge(START, "node_a") workflow.add_edge("node_a", "node_b") workflow.add_edge("node_b", END) checkpointer = InMemorySaver() graph = workflow.compile(checkpointer=checkpointer) config: RunnableConfig = {"configurable": {"thread_id": "1"}} graph.invoke({"foo": "", "bar":[]}, config)

Checkpoints created in simple graph example

When invoking a simple sequential graph (START -> node_a -> node_b -> END), exactly 4 checkpoints are saved: Empty checkpoint with START as next node; Checkpoint with user input and node_a as next node; Checkpoint with node_a outputs and node_b as next node; Checkpoint with node_b outputs and no next nodes (graph complete). The bar channel values contain outputs from both nodes because it has a reducer that accumulates values.

Access checkpoint namespace from within a node

From within a node, access the checkpoint namespace via config: from langchain_core.runnables import RunnableConfig def my_node(state: State, config: RunnableConfig): checkpoint_ns = config["configurable"]["checkpoint_ns"] # "" for the parent graph, "node_name:uuid" for a subgraph

Example passing durability mode to graph execution

graph.stream( {"input": "test"}, durability="sync" ) Or in JavaScript: await graph.stream( { input: "test" }, { durability: "sync" } )

Checkpointer conformance test suite

langgraph-checkpoint-conformance validates checkpointer implementations against the full contract including delta channel history. Install with: pip install langgraph-checkpoint-conformance. Use the @checkpointer_test decorator to define a test fixture that yields the checkpointer instance, then call await validate(test_fixture) to run tests. The suite auto-detects extended capabilities (like aget_delta_channel_history) and runs relevant tests for each. Run as part of CI before shipping to validate base methods and extended capabilities.

BaseCheckpointSaver custom implementation required methods

When subclassing BaseCheckpointSaver for a custom storage backend, implement these five required methods: async def aput(self, config, checkpoint, metadata, new_versions); async def aput_writes(self, config, writes, task_id, task_path=""); async def aget_tuple(self, config); async def alist(self, config, *, filter=None, before=None, limit=None); async def adelete_thread(self, thread_id). All are required—missing base method raises NotImplementedError at runtime. For synchronous execution, implement sync versions (put, put_writes, get_tuple, list, delete_thread).

Delta channel history retrieval for state reconstruction

When loading a checkpoint with delta channels absent from channel_values, LangGraph calls saver.get_delta_channel_history(config=config, channels=[...]) which returns for each channel: writes (all writes in ancestor chain oldest first up to nearest snapshot) and optionally seed (stored _DeltaSnapshot blob at nearest ancestor, absent if walk reaches root). The runtime then calls channel.from_checkpoint(seed) and channel.replay_writes(writes) to reconstruct the live value.

MISSING_CHECKPOINTER error occurs when checkpointer not provided to compile()

The MISSING_CHECKPOINTER error is raised when attempting to use built-in LangGraph persistence without providing a checkpointer to the compile() method of StateGraph or @entrypoint.

How to fix MISSING_CHECKPOINTER: pass checkpointer to compile()

To resolve the MISSING_CHECKPOINTER error, initialize a checkpointer such as InMemorySaver and pass it to the compile() method of StateGraph or to the @entrypoint decorator. For StateGraph: graph = StateGraph(...).compile(checkpointer=checkpointer). For @entrypoint: @entrypoint(checkpointer=checkpointer).

InMemorySaver checkpointer example for Python

In Python, initialize InMemorySaver from langgraph.checkpoint.memory and pass it to compile() or @entrypoint: from langgraph.checkpoint.memory import InMemorySaver; checkpointer = InMemorySaver(); graph = StateGraph(...).compile(checkpointer=checkpointer)

InMemorySaver checkpointer example for TypeScript

In TypeScript, import InMemorySaver from @langchain/langgraph and pass it to compile() or entrypoint: import { InMemorySaver, StateGraph } from "@langchain/langgraph"; const checkpointer = new InMemorySaver(); const graph = new StateGraph(...).compile({ checkpointer });

LangGraph API handles persistence without manual checkpointer configuration

As an alternative to manually configuring checkpointers, you can use the LangGraph API which handles all persistence infrastructure automatically without requiring manual checkpointer implementation or configuration.

Checkpointing differences between Functional API and Graph API

Both APIs generate and use checkpoints. In the Graph API a new checkpoint is generated after every superstep. In the Functional API, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.

Determinism in workflow replay

When you resume a workflow run the code does NOT resume from the same line of code where execution stopped. Execution returns to a checkpoint boundary and the workflow replays forward until it reaches the pause again. For the Functional API replay starts at the beginning of the entrypoint while LangGraph restores completed task and subgraph results from the checkpointer instead of recomputing them. This preserves the recorded order of steps across pauses including for long-running or non-deterministic task outputs. Different runs of a workflow can produce different results but resuming a specific thread should replay the same persisted task and subgraph results.

Determinism guidelines for human-in-the-loop workflows

To use features like human-in-the-loop you must place non-deterministic work (for example random values) and side effects (for example file writes or API calls) in tasks. To ensure that your workflow is deterministic and can be consistently replayed follow these guidelines: Avoid repeating work - in an entrypoint if you chain several side effects (logging file writes network calls) give each its own task so resume restores their outputs from the checkpointer instead of running them again; Encapsulate non-deterministic operations - keep values that can change between attempts (random numbers or wall-clock reads) inside tasks so replay lines up with what was checkpointed; Use idempotent operations - for partial task failures and retries design operations to be idempotent.

Interrupts require checkpointer and thread_id for resuming

To use interrupt(), you need: 1) A checkpointer to persist the graph state (use a durable checkpointer in production); 2) A thread_id in config (config={"configurable": {"thread_id": ...}} in Python or {configurable: {thread_id: ...}} in JavaScript) so the runtime knows which state to resume from.

thread_id acts as a persistent cursor

The thread_id you choose is effectively your persistent cursor. Reusing the same thread_id resumes the same checkpoint; using a new thread_id value starts a brand-new thread with an empty state.

Human-in-the-loop in LangGraph

LangGraph enables human-in-the-loop workflows by allowing users to incorporate human oversight by inspecting and modifying agent state at any point.

Persistence benefit in LangGraph

LangGraph provides persistence capabilities that enable building agents that persist through failures and can run for extended periods, resuming from where they left off.

Persistence layer overview

Persistence lets LangGraph applications keep useful information beyond a single graph run. It matters when an agent needs to continue a conversation, resume after an interruption, recover from a failure, or remember information across interactions.

Passing thread_id in graph invoke configuration

Pass a thread_id in the configurable section of graph config to specify which thread to use. Example: graph.invoke({"messages": [{"role": "user", "content": "Hi, my name is Bob."}]}, {"configurable": {"thread_id": "thread-1"}}).

Checkpointer vs Store comparison table

Checkpointers persist graph state snapshots with a single thread scope for short-term, thread-scoped memory used for conversation continuity, human-in-the-loop, time travel, and fault tolerance. Access pattern: pass thread_id in graph config. Stores persist application-defined key-value data across threads with long-term, cross-thread memory scope used for user preferences, facts, and shared knowledge. Access pattern: read and write items from nodes or application code.

PostgresSaver thread_id character limit

When using PostgresSaver or AsyncPostgresSaver, the thread_id is stored in a column with limited length. Keep thread_id values under 255 characters. If your thread_id exceeds the column size, you will see a database error. Fix: Use a UUID or hash if you need deterministic IDs.

MemorySaver does not persist between restarts

MemorySaver and InMemorySaver store checkpoints in RAM. When the process restarts, all checkpoints are lost. For production, use a persistent checkpointer like PostgresSaver (PostgreSQL with async support) or SqliteSaver (local file-based storage for development).

Give your agent this brain