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