Skip to main content
Articles/langchain/langgraph-memory-and-state-persistence
Series: Human-in-the-Loop & Multi-Agent systems•Part 1 of 3
Scenario Article

LangGraph Memory and State Persistence: Checkpointers, Threads, and Cross-Session Memory

The Amnesia Problem: Why Stateless Agents Frustrate Users

Abstract Algorithms•Mar 28, 2026
💡 Executive Summary

Checkpointers + thread IDs give LangGraph agents persistent memory across turns and sessions.

🎯 System Scenario & Requirements

System Goal

Checkpointers + thread IDs give LangGraph agents persistent memory across turns and sessions.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • TLDR: Checkpointers + thread IDs give LangGraph agents persistent memory across turns and sessions.
  • Your customer support agent is on its third message with a user.
  • The user says: "As I mentioned before, my order number is 9847." The agent replies: "Could you please provide your order number so I can look that up?" The agent hasn't forgotten.

Constraints & Assumptions

  • Storage Costs Compound Quickly What happens on Turn 2: LangGraph loads the checkpoint from Turn 1 (which already has Alice's first message and the assistant's reply).
  • The new HumanMessage is appended via operator.add .

⚡ Execution Stages & Decision Trace

1

Stage 1 — Checkpointers: InMemorySaver, SqliteSaver, and PostgresSaver

Action / Decision:

A checkpointer is an object you attach at compile() time.

Component Behavior:

After every node runs, LangGraph calls the checkpointer to write the current state to storage.

Observable Output: On the next invoke() with the same thread ID, LangGraph calls the checkpointer to read the latest snapshot before the graph starts running.
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

Scenario context

Checkpointers + thread IDs give LangGraph agents persistent memory across turns and sessions.

Your customer support agent is on its third message with a user. The user says: "As I mentioned before, my order number is 9847." The agent replies: "Could you please provide your order number so I can look that up?"

The agent hasn't forgotten. It never knew. Every call to graph.invoke() without a checkpointe…

This walkthrough focuses on The Amnesia Problem: Why Stateless Agents Frustrate Users.

Before making a design choice, clarify:

  • Your customer support agent is on its third message with a user.
  • The user says: "As I mentioned before, my order number is 9847." The agent replies: "Could you please provide your order number so I can look that up?" The agent hasn't forgotten.
  • Every call to graph.invoke() without a checkpointer starts with a completely empty state — zero messages, zero context, no trace of anything said before.

Concepts in play: llm-agents-and-langgraph, graph-traversal-algorithms, api-gateway, jvm-internals-and-garbage-collection

Stage 1 — Checkpointers: InMemorySaver, SqliteSaver, and PostgresSaver

  • A checkpointer is an object you attach at compile() time.
  • After every node runs, LangGraph calls the checkpointer to write the current state to storage.
  • On the next invoke() with the same thread ID, LangGraph calls the checkpointer to read the latest snapshot before the graph starts running.
  • LangGraph ships three checkpointers out of the box: InMemorySaver — Development and Unit Tests State lives in a Python dictionary in RAM.
from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
from langgraph.checkpoint.sqlite import SqliteSaver

# Context-manager handles connection lifecycle
with SqliteSaver.from_conn_string("./agent_memory.db") as checkpointer:
    graph = builder.compile(checkpointer=checkpointer)
    result = graph.invoke({"messages": [...]}, config=config)

Observable outcome: LangGraph ships three checkpointers out of the box: InMemorySaver — Development and Unit Tests State lives in a Python dictionary in RAM.

Stage 2 — Deep Dive: How LangGraph Checkpointing Works Under the Hood

  • The Internals When LangGraph compiles a graph with a checkpointer, it wraps every node execution in a checkpoint lifecycle: 1.
  • Before the graph runs: LangGraph calls checkpointer.get tuple(config) .
  • If a checkpoint exists for the given thread id , it deserializes the state and uses it as the starting values.
  • If not, the graph starts fresh.
# The config dict is the entire addressing scheme for LangGraph persistence
config = {
    "configurable": {
        "thread_id": "user-alice-session-42",   # conversation namespace
        "checkpoint_id": None,                   # None = latest; set a UUID to time-travel
    }
}

Observable outcome: If not, the graph starts fresh.

Stage 3 — Thread Isolation and State Flow

  • The diagram below shows two users — Alice and Bob — both interacting with the same compiled graph.
  • Their state never touches because thread id keeps every checkpoint in its own namespace within the same checkpointer backend.
  • Two conversations share one graph instance and one checkpointer backend, but their state is completely isolated by thread id .
  • Each arrow to the checkpointer is a separate row in the checkpoints table.
graph TD
    subgraph Alice[Thread: user-alice]
        A1([invoke turn 1]) --> A2[classify_intent node]
        A2 --> A3[fetch_order node]
        A3 --> A4[respond node]
        A4 --> A5([invoke turn 2 resumes from A4 snapshot])
    end

    subgraph Bob[Thread: user-bob]
        B1([invoke turn 1]) --> B2[classify_intent node]
        B2 --> B3[respond node]
        B3 --> B4([invoke turn 2 resumes from B3 snapshot])
    end

    CP[(Checkpointer PostgresSaver)]

    A4 -- "writes checkpoint thread=user-alice" --> CP
    B3 -- "writes checkpoint thread=user-bob" --> CP
    A5 -- "reads checkpoint thread=user-alice" --> CP
    B4 -- "reads checkpoint thread=user-bob" --> CP
# Snapshot of Alice's current conversation state
state = graph.get_state({"configurable": {"thread_id": "user-alice"}})
print(state.values["messages"])   # full message list
print(state.next)                 # next node(s) that would run

# Full audit trail — every checkpoint in reverse-chronological order
history = list(graph.get_state_history({"configurable": {"thread_id": "user-alice"}}))
for snapshot in history:
    print(f"Step {snapshot.config['configurable']['checkpoint_id']}: "
          f"next={snapshot.next}, messages={len(snapshot.values['messages'])}")

Observable outcome: Each arrow to the checkpointer is a separate row in the checkpoints table.

Stage 4 — Real-World Applications: How Production Agents Use Persistent Memory

  • Case Study 1 — E-Commerce Support Agent An online retailer runs a LangGraph support agent with PostgresSaver .
  • When a customer opens a chat: - Input: thread id = f"customer-{customer id}-{ticket id}" - Process: The graph loads prior messages for this ticket.
  • If the customer said "I already tried resetting my password" three turns ago, the agent routes to escalation rather than suggesting the same fix again.
    • Output: First-contact resolution rate improved ~18% in one deployment because agents stopped repeating questions.
# Agent drafts report, hits "awaiting_approval" interrupt node, and pauses
result = graph.invoke({"messages": [...]}, config=config)
# result.next == ("awaiting_approval",) — graph is suspended

# Hours later, manager approves via dashboard — graph resumes from exact checkpoint
graph.invoke(
    Command(resume={"approved": True, "approver": "manager@co.com"}),
    config=config
)

Observable outcome: - Output: First-contact resolution rate improved ~18% in one deployment because agents stopped repeating questions.

Stage 5 — LangGraph Memory Store: Long-Term Semantic Memory Beyond the Conversation

  • The BaseStore interface is LangGraph's answer to memory that outlives any single thread.
  • It is a key-value store organized into namespaces (tuples of strings) with keys (string identifiers) and values (arbitrary dicts).
  • For production, replace InMemoryStore with AsyncPostgresStore : Message Trimming to Prevent Context Overflow As conversations grow, trim old messages before passing them to the LLM.
  • LangChain's trim messages utility handles this cleanly: For very long-running agents, combine trimming with a summarization node that periodically condenses old messages into a single summary AIMessage , reducing histor…
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()

# Write a user preference (namespace, key, value)
store.put(("user_profiles", "alice"), "preferences", {
    "language": "en",
    "contact_channel": "email",
    "last_order": "9847"
})

# Read it back in any future session
results = store.search(("user_profiles", "alice"))
if results:
    prefs = results[0].value
    print(prefs["contact_channel"])   # "email"

# Update — put() is upsert, same namespace+key overwrites
store.put(("user_profiles", "alice"), "preferences", {
    **prefs,
    "last_order": "10234"
})
from langgraph.store.postgres.aio import AsyncPostgresStore

store = AsyncPostgresStore.from_conn_string(DB_URI)
await store.setup()   # creates the store table on first run

Observable outcome: LangChain's trim messages utility handles this cleanly: For very long-running agents, combine trimming with a summarization node that periodically condenses old messages into a single summary AIMessage , reducing histor…

End-to-end outcome

After working through the stages, you should be able to explain:

  • Pick your checkpointer before you write any agent code.
  • Switching checkpointers later means migrating stored state — InMemorySaver checkpoints cannot be imported into SqliteSaver .

Alternatives considered

Memory Fundamentals: Short-Term State vs Long-Term Store vs External Memory

LangGraph offers three distinct memory layers, and conflating them is the most common source of confusion when building persistent agents.

Failure and degraded modes

Trade-offs and Failure Modes: Storage Costs, Context Overflow, and Stale Memory

  • Detection: Storage Costs Compound Quickly What happens on Turn 2: LangGraph loads the checkpoint from Turn 1 (which already has Alice's first message and the assistant's reply).
  • User or operator impact: Storage Costs Compound Quickly What happens on Turn 2: LangGraph loads the checkpoint from Turn 1 (which already has Alice's first message and the assistant's reply).
First article in seriesNext Article →

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit