LangGraph memory: checkpointers, stores and long-term memory

LangGraph has two kinds of memory. A checkpointer saves a thread's graph state at every step, which gives you short-term memory, resumable runs and recovery from failures. A store saves data outside the graph state that any thread can read, which gives you long-term memory such as a user's preferences (LangGraph docs).
Most applications use both. An external memory service such as supermemory can take the store's place when you want facts pulled out of conversations, kept current and searched for you. Everything below was checked against LangGraph's docs on October 16, 2026, when langgraph 1.2.15 was current.
Checkpointer vs store: what's the difference?
| Checkpointer | Store | |
|---|---|---|
| What it saves | Snapshots of the graph state | Data your app chooses, as JSON documents under a namespace and key |
| Scope | One thread | Every thread |
| What it gives you | Conversation continuity, human-in-the-loop, time travel, recovery after a failure | User preferences, facts and shared knowledge |
| How you use it | Pass a thread_id in the config |
Read and write it from nodes through runtime.store |
A new thread starts with empty state, so anything the user should find again in their next conversation has to come from the store, or from a memory service, not from the checkpointer.
How do you share memory across threads in LangGraph?
Compile the graph with both, and pass the user's identity in as context for each run. This example needs no model or network, so you can run it as a test:
from dataclasses import dataclass
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.runtime import Runtime
from langgraph.store.memory import InMemoryStore
@dataclass
class Context:
user_id: str # resolved from your auth for each request
class State(TypedDict):
answer: str
def recall(state: State, runtime: Runtime[Context]):
item = runtime.store.get((runtime.context.user_id, "preferences"), "timezone")
return {"answer": item.value["value"] if item else "unknown"}
store = InMemoryStore()
store.put(("user-a", "preferences"), "timezone", {"value": "Europe/London"})
builder = StateGraph(State, context_schema=Context)
builder.add_node("recall", recall)
builder.add_edge(START, "recall")
builder.add_edge("recall", END)
app = builder.compile(checkpointer=InMemorySaver(), store=store)
first = app.invoke({}, {"configurable": {"thread_id": "thread-1"}}, context=Context(user_id="user-a"))
second = app.invoke({}, {"configurable": {"thread_id": "thread-2"}}, context=Context(user_id="user-a"))
other = app.invoke({}, {"configurable": {"thread_id": "thread-3"}}, context=Context(user_id="user-b"))
assert first["answer"] == second["answer"] == "Europe/London"
assert other["answer"] == "unknown"
Two threads for the same user find the preference, and a third user doesn't. Reading the store and the user through the Runtime object, the way LangGraph's memory docs recommend, keeps one request's user from leaking into another's, which a global variable can't promise once requests run side by side.
Both backends here live in memory, so they show thread and user scope inside one process, not persistence after a restart. InMemorySaver loses everything when the process stops. In production, use database-backed versions such as PostgresSaver and PostgresStore (persistence docs).
How do you add supermemory to a LangGraph agent?
A store keeps exactly what you put in it. supermemory takes the conversation itself, pulls out the facts, keeps them current when they change, builds a profile for each user, and returns what's relevant to the next question (how it works). In a graph, that's two nodes around the model: recall before it answers, save after. This follows the LangGraph integration docs for the v5 SDK:
from dataclasses import dataclass
from typing import Annotated, TypedDict
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.runtime import Runtime
from supermemory import Supermemory # pip install supermemory (v5 SDK, 5.0.0 or later)
llm = ChatOpenAI(model="gpt-5.5")
memory = Supermemory() # reads SUPERMEMORY_API_KEY
@dataclass
class Context:
user_id: str # the namespace: one per user
thread_id: str # one supermemory document per conversation
class State(TypedDict):
messages: Annotated[list, add_messages]
memory_context: str
def recall(state: State, runtime: Runtime[Context]):
user = runtime.context.user_id
query = state["messages"][-1].content
profile = memory.profile(user).profile
found = memory.search(user, query=query, search_mode="memories", limit=5).results
facts = [m.memory for m in profile.static + profile.dynamic] + [r.memory for r in found if r.memory]
return {"memory_context": "\n".join(f"- {f}" for f in facts)}
def respond(state: State):
system = SystemMessage(content=f"You are a helpful assistant. What you know about this user:\n{state['memory_context']}")
return {"messages": [llm.invoke([system] + state["messages"])]}
def save(state: State, runtime: Runtime[Context]):
turn = f"user: {state['messages'][-2].content}\nassistant: {state['messages'][-1].content}"
memory.add(runtime.context.user_id, content=turn, id=runtime.context.thread_id)
return {}
builder = StateGraph(State, context_schema=Context)
builder.add_node("recall", recall)
builder.add_node("respond", respond)
builder.add_node("save", save)
builder.add_edge(START, "recall")
builder.add_edge("recall", "respond")
builder.add_edge("respond", "save")
builder.add_edge("save", END)
app = builder.compile(checkpointer=InMemorySaver())
app.invoke(
{"messages": [{"role": "user", "content": "Plan my week around the launch"}]},
{"configurable": {"thread_id": "chat_42"}},
context=Context(user_id="user_123", thread_id="chat_42"),
)
The checkpointer still owns the thread: this conversation's messages and where the graph got to. supermemory owns what should outlive the thread. A new thread for user_123 starts with empty state, and the recall node fills in what that user told you before. Get an API key from the supermemory console, and see pricing for what saves and searches cost.
Keep these as visible stages in the graph, so each one can be tested and logged on its own:
| Stage | What it should do |
|---|---|
| Resolve identity | Get an authorized user or tenant from your auth, before the graph runs |
| Recall | Fetch a bounded amount of relevant background |
| Answer | Combine the current messages with the recalled facts, marked as background |
| Save | Store only what your retention policy allows |
| Observe | Record IDs, status and timing, without logging sensitive content |
Why does the save need a stable ID?
A checkpoint records how far the graph got. A write to supermemory is a separate side effect, and the two don't commit together. If a run fails after the save node sends its write but before the next checkpoint lands, resuming the thread can run the save again.
That's why the save uses the thread ID as the document id. In the v5 API, sending content again with the same id adds to or diffs that document instead of creating a copy, and unchanged content isn't billed twice (v5 guide, billing docs). Derive IDs from the source action, never from a random value generated on each retry, and keep the ID supermemory returns so a later correction or deletion reaches the right record.
When should writes wait, and when can they run in the background?
New memories form in the background after a save returns, so a new namespace can look empty at first (v5 guide). Plan for that gap instead of fighting it:
- If the next node needs a fact from this turn, carry it in the thread state instead of searching supermemory for something that may not have formed yet.
- If the next request needs it, save with
dreaming="instant", which processes each document right away, for one extra operation per document. - For everything else, let processing run in the background, and don't make every step wait for ingestion.
- Treat "not found yet" differently from "doesn't exist". Check the document's
system.dreamingStatusrather than reading an empty search as a permanent answer.
For background writes, use a queue that survives a worker restart and tells retryable service failures apart from invalid input. The ingestion guide covers freshness and retries in more depth.
What should you test before production?
Run these against your real checkpointer, store and memory service, not the in-memory ones:
- A restart. Stop the process, reconnect to the configured backend, and resume a known thread. Check message order, tool results and the next scheduled step.
- A replay around the write. Stop the run just before the save, just after it, and after the next checkpoint. The application should tell work that never happened apart from work whose acknowledgment was lost, and no case should create a duplicate.
- Shared memory across threads. Use two threads for one fictional user and a third for another user. Shared context should reach only the first two.
- A correction. Correct the shared fact, then resume an old thread, and check the old checkpoint's context doesn't win over the new memory.
- A deletion. Delete the source and confirm the fact no longer comes back.
Measure recall and latency with your own model, checkpointer, data and expected load. The memory debugging guide helps when one of these fails, and the isolation guide covers keeping tenants apart.
Common questions
Does LangGraph have long-term memory?
Yes. Stores are LangGraph's long-term memory: they hold data across threads under namespaces such as a user ID, and you read and write them from nodes. A memory service such as supermemory is the other option, when you want facts extracted and kept current for you.
What's the difference between a checkpointer and a store?
A checkpointer saves one thread's graph state, so a conversation can continue or recover. A store saves data that every thread can read, such as a user's preferences.
Does MemorySaver persist between restarts?
No. MemorySaver and InMemorySaver keep checkpoints in RAM, so a restart loses them. Use a persistent checkpointer such as PostgresSaver, or SqliteSaver for local development.
Can I use supermemory with a LangChain agent instead of a graph?
Yes. The LangChain memory guide shows the same recall-and-save pattern with create_agent middleware. For the same loop without a framework, see the quickstart.