How to Fix 'memory not persisting' in LangGraph (Python)

By Cyprian AaronsUpdated 2026-04-21
memory-not-persistinglanggraphpython

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 patternFixed pattern
Graph has no checkpointerGraph compiled with a checkpointer
No thread_id in configthread_id passed on every invoke
State resets every callState 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

  1. Check whether your graph was compiled with a checkpointer

    • Search for compile(checkpointer=...).
    • If it’s missing, persistence will not work across invocations.
  2. 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.
  3. 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.
  4. 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.

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_id values per conversation/session.
  • Treat state schema and node outputs as contracts:
    • For MessagesState, always return message lists under "messages".
  • Separate dev memory from production memory:
    • Use MemorySaver() locally.
    • Use durable storage in deployed apps so restarts don’t wipe conversations.

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

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

Related Guides