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