LangGraph has three distinct memory concepts that confuse almost every builder. Here's the plain-English guide.

The Confusion

Ask any LangGraph beginner about memory and you'll hear the same story: 'I added a checkpointer and thought my agent had memory, but it still forgets everything between conversations.' Or the reverse: 'I'm using threads but my agents are sharing state across users.'

LangGraph has three distinct memory concepts -- checkpointers, threads, and stores -- and they work at different levels of persistence and scope. Confusing them causes bugs that are very hard to debug because everything appears to work locally but fails in unexpected ways when users interact with the agent.

This article explains each concept clearly, with code, and tells you exactly when to use each one.

The Three Concepts at a Glance

Concept What it stores Scope Persists across restarts?
Checkpointer The full graph state at each execution step Per thread Depends on backend (InMemory: No, SQLite/Postgres: Yes)
Thread A unique ID that groups a series of checkpoints into one conversation Per user session As long as checkpoints exist
Store Arbitrary key-value data independent of graph execution Cross-thread (global or per-namespace) Yes, with persistent backend

Checkpointers: State Within a Conversation

A checkpointer saves a snapshot of your graph's state after every node execution. This is what enables human-in-the-loop workflows (pause, wait for input, resume), fault tolerance (resume after a crash), and multi-turn conversations (remember what was said earlier in this session).

Three checkpointer backends

Backend Module Persists after restart? Use for
InMemorySaver langgraph.checkpoint.memory NO Development and testing only
SqliteSaver langgraph.checkpoint.sqlite YES Single-instance production, small scale
PostgresSaver langgraph.checkpoint.postgres YES Production, multi-instance, high scale
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.postgres import PostgresSaver
 
# Development
checkpointer = InMemorySaver()
 
# Single-server production
checkpointer = SqliteSaver.from_conn_string("checkpoints.db")
 
# Multi-server / cloud production
checkpointer = PostgresSaver.from_conn_string(
    "postgresql://user:pass@host/dbname"
)
 
# Compile graph with checkpointer
graph = builder.compile(checkpointer=checkpointer)
InMemorySaver is reset every time your process restarts. If you're using it in a FastAPI server, every deployment wipes all conversation history. Switch to SqliteSaver or PostgresSaver before any production deployment.

Threads: Separating Conversations

A thread is simply a unique ID that namespaces a series of checkpoints. When you invoke a graph with a thread_id, LangGraph loads the latest checkpoint for that thread and resumes from there. Different thread IDs are completely isolated -- one thread cannot see another's state.

Thread IDs are how you separate conversations between different users, or different sessions for the same user.

# Every invoke must include a config with thread_id
config = {"configurable": {"thread_id": "user-123-session-456"}}
 
# First message -- creates a new thread
result = graph.invoke(
    {"messages": [HumanMessage(content="Hello")]},
    config=config
)
 
# Second message -- resumes the SAME thread, agent has memory
result = graph.invoke(
    {"messages": [HumanMessage(content="What did I just say?")]},
    config=config
)
 
# Different user -- completely separate thread, no shared state
other_config = {"configurable": {"thread_id": "user-789-session-001"}}
result = graph.invoke(
    {"messages": [HumanMessage(content="Hello")]},
    config=other_config
)
Generate thread IDs that encode useful information: user_id + session_id, or a UUID stored in your session store. This makes it easy to retrieve a user's conversation history later.

The most common thread mistake

Hardcoding a single thread ID for all users. If thread_id is a static string like 'my-agent', every user shares the same conversation history -- each message from any user is visible to all other users. Always generate a unique thread ID per user or per session.

Stores: Long-Term Memory Across Conversations

Checkpointers and threads give you memory within a conversation. A Store gives you memory that persists across conversations -- information about a user that should be available regardless of which thread they're in.

Examples: a user's name and preferences, facts established in previous sessions, a running list of tasks or goals, knowledge accumulated over time.

from langgraph.store.memory import InMemoryStore
# or for production: from langgraph.store.postgres import PostgresStore
 
store = InMemoryStore()
 
# Your agent can write to the store during a conversation
# (inject the store into your node functions)
def my_node(state, config, *, store):
    user_id = config["configurable"]["user_id"]
 
    # Write a fact about the user
    store.put(
        namespace=("users", user_id),
        key="preferences",
        value={"language": "English", "timezone": "UTC"}
    )
 
    # Read it back (in any future conversation)
    prefs = store.get(namespace=("users", user_id), key="preferences")
    return state
What you need Use this
Remember conversation history within one session Checkpointer + Thread ID
Separate state between different users Unique Thread IDs per user/session
Remember facts about a user across all sessions Store
Share knowledge across all agents and all users Store with a shared namespace
Pause and resume a workflow (human-in-the-loop) Checkpointer with interrupt

Putting It Together: A Production Memory Setup

from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
from langgraph.graph import StateGraph, MessagesState
 
# Production setup: PostgreSQL for both checkpoints and store
DB_URL = "postgresql://user:pass@host/dbname"
checkpointer = PostgresSaver.from_conn_string(DB_URL)
store = PostgresStore.from_conn_string(DB_URL)
 
# Build and compile with both
builder = StateGraph(MessagesState)
# ... add nodes and edges ...
graph = builder.compile(checkpointer=checkpointer, store=store)
 
# Invoke with user context
config = {
    "configurable": {
        "thread_id": f"user-{user_id}-session-{session_id}",
        "user_id": user_id,   # available inside nodes via config
    }
}
result = await graph.ainvoke({"messages": [user_message]}, config)

Quick Reference

  • Checkpointer = memory within a conversation (per thread)
  • Thread ID = what separates one conversation from another
  • Store = memory across conversations (facts, preferences, knowledge)
  • Never use InMemorySaver in production -- it wipes on restart
  • Always generate unique thread IDs per user/session -- never hardcode
  • Use PostgresSaver + PostgresStore together for production multi-user deployments