Skip to content
Blog

Checkpointing LangGraph State: Snapshots, Restores, and Forks

Deep dive into LangGraph checkpointing. Create snapshots of agent state, restore from any point, and fork executions to explore alternative paths.

Published on September 8, 2026

AI Assistant

LangGraph checkpointing is more than crash recovery — it’s a time machine for your agents. Snapshots capture the full state at any point. Restores let you resume from any snapshot. Forks let you branch from any snapshot to explore alternatives. Together, they give you complete control over agent execution.

The Checkpoint Model

Every super-step in a LangGraph execution produces a checkpoint:

Step 0: Initial State → Checkpoint 0

Step 1: After Agent Node → Checkpoint 1

Step 2: After Tool Node → Checkpoint 2

Step 3: After Synthesis → Checkpoint 3

Each checkpoint contains:

  • State values — The full state dictionary
  • Metadata — Timestamp, step number, parent checkpoint ID
  • Versions — For schema migration support

Creating Snapshots

Automatic Snapshots

LangGraph automatically creates checkpoints at every super-step:

from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver

graph = StateGraph(MessagesState)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

# Every execution creates checkpoints automatically
config = {"configurable": {"thread_id": "session-1"}}
result = app.invoke({"messages": [("user", "Research topic")]}, config)

Manual Snapshots

Create explicit snapshots at important points:

from langgraph.checkpoint.base import create_checkpoint

class SnapshotManager:
    def __init__(self, checkpointer):
        self.checkpointer = checkpointer
    
    async def create_snapshot(
        self,
        config: dict,
        label: str = None
    ) -> str:
        """Create a named snapshot of the current state."""
        # Get current state
        state = await self.checkpointer.aget(config)
        
        # Create checkpoint with label
        checkpoint = create_checkpoint(
            state.config,
            state.metadata | {"label": label, "snapshot_time": datetime.now().isoformat()}
        )
        
        # Store the snapshot
        await self.checkpointer.aput(config, checkpoint)
        
        return checkpoint["id"]

# Usage
snapshot_mgr = SnapshotManager(checkpointer)
snapshot_id = await snapshot_mgr.create_snapshot(
    config,
    label="before_expensive_operation"
)

Restoring from Snapshots

Resume from Latest Checkpoint

# Resume execution from the last checkpoint
result = app.invoke(None, config)  # None = load from checkpoint

Resume from Specific Checkpoint

# List all checkpoints
history = list(app.get_state_history(config))

# Resume from checkpoint #2
specific_config = {
    "configurable": {
        "thread_id": "session-1",
        "checkpoint_id": history[2].config["configurable"]["checkpoint_id"]
    }
}

result = app.invoke(None, specific_config)

Restore with Modifications

# Load state, modify it, then continue
state = app.get_state(config)

# Modify the state
modified_messages = state.values["messages"] + [
    ("user", "Actually, let's try a different approach")
]

# Update state and continue
app.update_state(config, {"messages": modified_messages})
result = app.invoke(None, config)

Forking: Exploring Alternative Paths

Basic Fork

# Get checkpoint history
history = list(app.get_state_history(config))

# Fork from an earlier checkpoint
fork_point = history[3]  # The 4th checkpoint

fork_config = {
    "configurable": {
        "thread_id": "session-1-fork",
        "checkpoint_id": fork_point.config["configurable"]["checkpoint_id"]
    }
}

# Continue from the fork with different input
app.update_state(fork_config, {
    "messages": [("user", "Try the alternative approach instead")]
})

fork_result = app.invoke(None, fork_config)

Parallel Forks

import asyncio

async def explore_alternatives(config, alternatives: list[str]):
    """Run multiple forks in parallel."""
    history = list(app.get_state_history(config))
    fork_point = history[-2]  # Fork from second-to-last checkpoint
    
    tasks = []
    for i, alt in enumerate(alternatives):
        fork_config = {
            "configurable": {
                "thread_id": f"fork-{i}",
                "checkpoint_id": fork_point.config["configurable"]["checkpoint_id"]
            }
        }
        
        app.update_state(fork_config, {
            "messages": [("user", alt)]
        })
        
        tasks.append(app.ainvoke(None, fork_config))
    
    results = await asyncio.gather(*tasks)
    return results

# Compare different approaches
results = await explore_alternatives(config, [
    "Approach A: Use vector search",
    "Approach B: Use keyword search",
    "Approach C: Combine both methods"
])

Practical Patterns

Debugging Failed Executions

def debug_failed_run(config):
    """Find where a run failed and what went wrong."""
    history = list(app.get_state_history(config))
    
    for i, checkpoint in enumerate(history):
        state = checkpoint.values
        
        # Check for errors
        if state.get("error"):
            print(f"Error at step {i}: {state['error']}")
            print(f"State before error:")
            print(json.dumps(state, indent=2, default=str))
            
            # Can fork from here to try again
            return checkpoint.config["configurable"]["checkpoint_id"]
    
    return None

# Find the failure point
failed_at = debug_failed_run(config)

# Fork from before the failure
if failed_at:
    fork_config = {
        "configurable": {
            "thread_id": "debug-fork",
            "checkpoint_id": failed_at
        }
    }
    # Modify state to fix the issue
    app.update_state(fork_config, {"retry_with_fix": True})
    result = app.invoke(None, fork_config)

A/B Testing Agent Strategies

class ABTestManager:
    def __init__(self, app):
        self.app = app
    
    async def test_strategies(
        self,
        base_config: dict,
        task: str,
        strategies: dict[str, dict]
    ) -> dict[str, dict]:
        """Test multiple strategies from the same starting point."""
        # Get the base state
        history = list(self.app.get_state_history(base_config))
        fork_point = history[0]  # Start from the beginning
        
        results = {}
        
        for strategy_name, modifications in strategies.items():
            fork_config = {
                "configurable": {
                    "thread_id": f"ab-{strategy_name}",
                    "checkpoint_id": fork_point.config["configurable"]["checkpoint_id"]
                }
            }
            
            # Apply strategy-specific modifications
            self.app.update_state(fork_config, modifications)
            
            # Run to completion
            result = await self.app.ainvoke(None, fork_config)
            
            results[strategy_name] = {
                "output": result["messages"][-1].content,
                "steps": len(result["messages"]),
                "tokens_used": result.get("token_count", 0),
            }
        
        return results

# Test different approaches
ab_test = ABTestManager(app)
results = await ab_test.test_strategies(
    base_config,
    "Research quantum computing applications",
    {
        "thorough": {"max_iterations": 20, "depth": "comprehensive"},
        "quick": {"max_iterations": 5, "depth": "brief"},
        "balanced": {"max_iterations": 10, "depth": "moderate"},
    }
)

Time-Travel Debugging

class TimeTravelDebugger:
    def __init__(self, app):
        self.app = app
    
    def inspect_trajectory(self, config):
        """Show the complete execution trajectory."""
        history = list(self.app.get_state_history(config))
        
        trajectory = []
        for i, checkpoint in enumerate(reversed(history)):
            state = checkpoint.values
            trajectory.append({
                "step": i,
                "timestamp": checkpoint.metadata.get("timestamp"),
                "messages_count": len(state.get("messages", [])),
                "last_message": str(state["messages"][-1].content)[:100] if state.get("messages") else "",
                "tool_calls": sum(
                    1 for m in state.get("messages", [])
                    if hasattr(m, "tool_calls") and m.tool_calls
                ),
            })
        
        return trajectory
    
    def compare_trajectories(self, config1, config2):
        """Compare two execution trajectories."""
        traj1 = self.inspect_trajectory(config1)
        traj2 = self.inspect_trajectory(config2)
        
        return {
            "trajectory_1": traj1,
            "trajectory_2": traj2,
            "step_difference": len(traj1) - len(traj2),
            "divergence_point": self._find_divergence(traj1, traj2),
        }
    
    def _find_divergence(self, traj1, traj2):
        """Find where two trajectories diverge."""
        for i, (s1, s2) in enumerate(zip(traj1, traj2)):
            if s1["last_message"] != s2["last_message"]:
                return i
        return min(len(traj1), len(traj2))

Production Considerations

Cleanup Old Checkpoints

class CheckpointCleanup:
    def __init__(self, checkpointer, retention_days: int = 30):
        self.checkpointer = checkpointer
        self.retention_days = retention_days
    
    async def cleanup_old_checkpoints(self):
        """Remove checkpoints older than retention period."""
        cutoff = datetime.now() - timedelta(days=self.retention_days)
        
        # For PostgreSQL checkpointer
        if hasattr(self.checkpointer, 'conn'):
            await self.checkpointer.conn.execute("""
                DELETE FROM checkpoints 
                WHERE created_at < $1
                AND NOT is_final  # Keep final checkpoints
            """, cutoff)

Schema Migration

class CheckpointMigration:
    def migrate_checkpoint(self, old_checkpoint: dict, old_version: str) -> dict:
        """Migrate a checkpoint to the current schema version."""
        if old_version == "1.0":
            # Add new fields
            old_checkpoint["metadata"]["version"] = "2.0"
            old_checkpoint["values"]["new_field"] = "default_value"
        
        return old_checkpoint

Conclusion

LangGraph checkpointing gives you snapshots for capturing state, restores for resuming execution, and forks for exploring alternatives. Together, they enable debugging, A/B testing, and time-travel workflows that would be impossible with simple state management. Use automatic checkpoints for safety, manual snapshots for important milestones, and forks for experimentation.