DEV Community

Mohammed Rafay
Mohammed Rafay

Posted on Originally published at memorysync.hashnode.dev

LangGraph memory explained: checkpointer vs store, and per-user memory across threads

Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official MemorySync and LangGraph docs.

If your LangGraph agent forgets everything the moment you start a new conversation, the graph is usually doing exactly what it was told. A checkpointer remembers a thread. It was never meant to remember a user. This post explains the split between the checkpointer and the store, then adds per-user memory that works across threads with MemorySync's LangGraph integration.

The LangGraph behaviour below comes from LangGraph's persistence docs and memory guide. Everything about MemorySync comes from its LangGraph Memory guide. The code is adapted from those docs. I haven't run it for this post, so check it against your versions.

Why the agent "forgets"

LangGraph has two persistence pieces, and they solve different problems:

Checkpointer Store
Memory type Short-term, scoped to one thread Long-term, shared across threads
How you reach it Pass a thread_id in the graph config Read and write items from nodes or app code
Typical contents The conversation so far, interrupts, time travel User preferences, facts, shared knowledge

So two things commonly go wrong:

  1. A new thread_id means a fresh state. The checkpointer brings back the history of thread-1 when you call thread-1 again. Call thread-2 and it starts empty. That's by design.
  2. MemorySaver / InMemorySaver live in RAM. LangGraph's docs say it directly: when the process restarts, every checkpoint is gone. For production they point you to a persistent checkpointer such as PostgresSaver.

Fixing (2) gets you threads that survive restarts. It doesn't fix (1). For "remember this user in every conversation" you need a store, and the store you get out of the box, InMemoryStore, also lives in memory.

The usual setup compiles the graph with both:

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore

graph = builder.compile(checkpointer=InMemorySaver(), store=InMemoryStore())
Enter fullscreen mode Exit fullscreen mode

Keep the checkpointer. The rest of this post swaps out the store and adds per-user recall.

Where MemorySync fits

MemorySync's guide divides the work the same way: thread state (checkpoints, resume, time travel) stays with a LangGraph checkpointer like langgraph-checkpoint-postgres, -sqlite or -redis. Long-term memory across threads goes to MemorySync. The integration has five pieces:

  • MemorySyncStore: a LangGraph BaseStore, passed in with store=.
  • MemorySyncMemoryMiddleware: injects context before the model call for create_agent (langchain 1.x).
  • create_memorysync_pre_model_hook: the same injection for create_react_agent.
  • create_persist_turn_node: a graph node that sends the latest user message to MemorySync, where durable facts are extracted.
  • create_memory_search_tool: a memory search tool the graph can call.

Step 1: Install

pip install "langchain-memorysync[langgraph]"
Enter fullscreen mode Exit fullscreen mode

Set MEMORYSYNC_API_KEY in your environment, or pass the key in explicitly. The guide lists langgraph 1.2+ and Python 3.10+. On Node.js the same exports ship in memorysync-langchain under the /langgraph subpath, which needs @langchain/langgraph 0.2+ and Node 18+. The examples here are Python.

Step 2: Swap the store and namespace it per user

Namespaces are string tuples, the same as in LangGraph. Put the user id in the namespace:

from langchain_memorysync.langgraph import MemorySyncStore

store = MemorySyncStore()  # reads MEMORYSYNC_API_KEY from the env

store.put(("memories", "user-1"), "diet", {"text": "vegetarian"})

item = store.get(("memories", "user-1"), "diet")
print(item.value)  # {'text': 'vegetarian'}

hits = store.search(("memories", "user-1"), query="what do they eat?")
print(hits[0].value, hits[0].score)
Enter fullscreen mode Exit fullscreen mode

According to the docs, each namespace maps to its own server-side scope, so ("memories", "user-a") and ("memories", "user-b") stay separate on the server rather than through a client-side filter. Two more things the guide notes:

  • No embedding config. LangGraph stores usually need an index config with an embedder and dimensions. MemorySync embeds stored values server-side, so search works without one.
  • Retries don't duplicate. LangGraph re-runs nodes after interrupts and errors. A key holds one live value: an identical retry is recognised server-side, and a changed value replaces the old one.

Store values are treated as framework state. They don't show up in MemorySync's memory list, search or recall.

Step 3: Compile it into an agent

Tools reach the store through get_store():

from langchain.agents import create_agent
from langchain_core.tools import tool
from langgraph.config import get_store
from langchain_memorysync.langgraph import MemorySyncStore

@tool
def save_preference(key: str, value: str) -> str:
    """Save a user preference to long-term memory."""
    get_store().put(("memories", "user-1"), key, {"text": value})
    return f"Saved {key}."

@tool
def recall_preferences(query: str) -> str:
    """Search the user's long-term memory."""
    hits = get_store().search(("memories", "user-1"), query=query)
    return "\n".join(h.value["text"] for h in hits) or "Nothing stored yet."

agent = create_agent(
    model,
    tools=[save_preference, recall_preferences],
    store=MemorySyncStore(),
)
Enter fullscreen mode Exit fullscreen mode

In a real app, replace the hard-coded "user-1" with your own stable user id.

Step 4: Recall per user without tool calls

Explicit save and recall tools only work when the model decides to call them. If you want the user's context to be there every turn, inject it before the model call. Use the middleware for create_agent or the pre-model hook for create_react_agent:

# create_agent (langchain 1.x)
from langchain.agents import create_agent
from langchain_memorysync.langgraph import MemorySyncMemoryMiddleware

agent = create_agent(
    model,
    tools=tools,
    middleware=[MemorySyncMemoryMiddleware(user_id="customer-7")],
)

# create_react_agent
from langgraph.prebuilt import create_react_agent
from langchain_memorysync.langgraph import create_memorysync_pre_model_hook

agent = create_react_agent(
    model,
    tools,
    pre_model_hook=create_memorysync_pre_model_hook(user_id="customer-7"),
)
Enter fullscreen mode Exit fullscreen mode

The docs say both only change what the model sees for that one call. Your checkpointed state isn't modified, so the stored transcript stays exactly what was said. If nothing relevant is found, the input passes through unchanged. Auto-persistence on the middleware is off by default.

Step 5: Save what the user tells you

To write facts from the conversation, add the persist node after your model node:

from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_memorysync.langgraph import create_persist_turn_node

remember = create_persist_turn_node(user_id="customer-7")

graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("remember", remember)
graph.add_edge(START, "agent")
graph.add_edge("agent", "remember")
graph.add_edge("remember", END)
app = graph.compile()

app.invoke({"messages": [("user", "I always fly aisle.")]},
           config={"configurable": {"thread_id": "trip-1"}})
Enter fullscreen mode Exit fullscreen mode

The session defaults to the graph's thread_id, so facts are grouped by conversation without extra wiring. If you're on create_agent, there's another option: MemorySyncMemoryMiddleware(user_id=..., auto_persist=True).

Step 6: Check it across two threads

This is the test the original question needed: two different thread_ids for the same user. It's adapted from the guide's complete example:

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_memorysync.langgraph import (
    MemorySyncMemoryMiddleware,
    MemorySyncStore,
    create_memory_search_tool,
)

USER = "customer-7"

agent = create_agent(
    ChatOpenAI(model="your-model"),
    tools=[create_memory_search_tool(user_id=USER)],
    middleware=[MemorySyncMemoryMiddleware(user_id=USER, auto_persist=True)],
    store=MemorySyncStore(),
)

agent.invoke(
    {"messages": [{"role": "user", "content": "I'm vegetarian and I fly aisle."}]},
    config={"configurable": {"thread_id": "monday"}},
)

out = agent.invoke(
    {"messages": [{"role": "user", "content": "Book my trip: flight plus a dinner spot."}]},
    config={"configurable": {"thread_id": "friday"}},
)
print(out["messages"][-1].content)
Enter fullscreen mode Exit fullscreen mode

The friday thread has no checkpoint history from monday. Any recall of the diet or seat preference has to come from the per-user memory, and the guide says that holds even if you stop the process between the two calls. If the second answer knows neither fact, look at user_id first. It has to be the same value in both calls.

The search tool is read-only and scoped to one end user. It returns readable strings instead of raising, because a tool exception would abort the whole graph run.

Recap

  • Checkpointer = this thread. Keep it, and use a persistent one (Postgres, SQLite, Redis) in production.
  • Store = across threads. Namespace it by user.
  • To make the agent remember the user every turn, inject context (middleware or pre-model hook) and persist facts (persist node or auto_persist=True).

Try it

If you want to see the save, reload and recall loop before wiring it into a graph, the MemorySync playground runs it in your browser: memorysync.io/try.

Top comments (0)