How to Fix 'memory not persisting' in LangGraph (Python)
If your LangGraph agent keeps “forgetting” previous turns, you’re usually not dealing with a model problem. You’re dealing with state persistence: the graph is running, but the memory backend, thread config, or state update pattern is wrong.
This shows up most often when you expect MessagesState or a checkpointer to preserve conversation history across invocations, but each call behaves like a fresh run.
The Most Common Cause
The #1 cause is simple: you built the graph without a checkpointer, or you forgot to pass thread_id in configurable.
In LangGraph Python, persistence is tied to a checkpointing backend such as MemorySaver, SqliteSaver, or another checkpointer. Without it, the graph has no place to store state between runs.
Broken vs fixed
| Broken pattern | Fixed pattern |
|---|---|
| Graph has no checkpointer | Graph compiled with a checkpointer |
No thread_id in config | thread_id passed on every invoke |
| State resets every call | State persists across calls |
# BROKEN
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
def assistant(state: MessagesState):
response = llm.invoke(state["messages"])
return {"messages": [response]}
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_edge(START, "assistant")
builder.add_edge("assistant", END)
graph = builder.compile() # no checkpointer
result1 = graph.invoke({"messages": [{"role": "user", "content": "My name is Sam"}]})
result2 = graph.invoke({"messages": [{"role": "user", "content": "What is my name?"}]})
# FIXED
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
checkpointer = MemorySaver()
def assistant(state: MessagesState):
response = llm.invoke(state["messages"])
return {"messages": [response]}
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant)
builder.add_edge(START, "assistant")
builder.add_edge("assistant", END)
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-123"}}
graph.invoke({"messages": [{"role": "user", "content": "My name is Sam"}]}, config=config)
result = graph.invoke({"messages": [{"role": "user", "content": "What is my name?"}]}, config=config)
If you see behavior like:
- •“It answers as if it never saw the previous message”
- •“The second invoke starts from scratch”
- •“Memory works in one call but not across requests”
then this is almost always the issue.
Other Possible Causes
1) You are using the wrong state update key
LangGraph expects your node to return updates in the shape your state schema understands. If you use MessagesState, return {"messages": [...]}. If you return a different key, the update may be ignored.
# WRONG
def assistant(state: MessagesState):
response = llm.invoke(state["messages"])
return {"message": [response]} # typo: message != messages
# RIGHT
def assistant(state: MessagesState):
response = llm.invoke(state["messages"])
return {"messages": [response]}
2) You are replacing messages instead of appending them
If your node returns only the latest AI message but your reducer isn’t configured correctly, you can accidentally wipe history.
# WRONG: overwrites intent if your state isn't set up for append semantics
return {"messages": response}
# RIGHT: return a list of new messages
return {"messages": [response]}
With MessagesState, LangGraph handles message accumulation correctly when you return a list of new messages.
3) Your thread IDs are changing between requests
This one bites people using FastAPI or background workers. If thread_id changes per request, LangGraph treats each request as a different conversation.
# WRONG: random thread id per request
config = {"configurable": {"thread_id": str(uuid.uuid4())}}
# RIGHT: stable id for the same conversation/user/session
config = {"configurable": {"thread_id": user_session_id}}
If you want persistence across an ongoing chat session, the same logical session must reuse the same ID.
4) You compiled with an in-memory saver and restarted the process
MemorySaver() stores checkpoints only in process memory. Restart the app and everything disappears.
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver() # ephemeral; lost on restart
graph = builder.compile(checkpointer=checkpointer)
For production use, switch to durable storage such as SQLite or Postgres-backed checkpointing.
5) You are invoking the wrong graph object
A common integration bug is compiling one graph with a checkpointer and then accidentally calling another instance without it.
graph_a = builder.compile(checkpointer=MemorySaver())
graph_b = builder.compile()
# WRONG: calling graph_b loses persistence
graph_b.invoke(inputs, config=config)
Make sure the object used by your API handler is the one compiled with checkpointing.
How to Debug It
- •
Check whether your graph was compiled with a checkpointer
- •Search for
compile(checkpointer=...). - •If it’s missing, persistence will not work across invocations.
- •Search for
- •
Log the exact config passed to
invoke()- •Confirm
{"configurable": {"thread_id": ...}}is present. - •Confirm that value stays stable across requests for the same chat session.
- •Confirm
- •
Inspect what your node returns
- •For
MessagesState, verify you return{"messages": [response]}. - •If you return malformed keys like
"message"or"history", updates won’t land where you expect.
- •For
- •
Print stored state after each turn
- •Use
get_state()on graphs that support it. - •If state exists after turn one but disappears on turn two, your thread ID or checkpointer setup is wrong.
- •Use
Example:
state = graph.get_state(config)
print(state.values)
If this prints empty or stale data after an invoke that should have persisted memory, focus on checkpointing first.
Prevention
- •Always define persistence explicitly:
- •Use a checkpointer at compile time.
- •Use stable
thread_idvalues per conversation/session.
- •Treat state schema and node outputs as contracts:
- •For
MessagesState, always return message lists under"messages".
- •For
- •Separate dev memory from production memory:
- •Use
MemorySaver()locally. - •Use durable storage in deployed apps so restarts don’t wipe conversations.
- •Use
If you’re debugging “memory not persisting” in LangGraph Python, start with these three checks in order:
- •Did I compile with a checkpointer?
- •Am I passing the same
thread_id? - •Am I returning updates in the right shape?
Nine times out of ten, one of those is broken.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit