When a chatbot built on LangChain suddenly forgets what the user said two messages ago, the culprit is almost always langchain memory losing state caused by object lifecycle mistakes or broken persistence. This how-to gives you a repeatable debugging sequence to pinpoint whether the memory object is being recreated each request, silently failing to serialize, or trampled by concurrent calls. You’ll end with a pytest that proves conversation state survives turns.
Step 1: Reproduce the loss in isolation
Write a minimal script that mirrors your production call pattern. The most frequent cause of langchain memory losing state is instantiating a fresh memory class inside the request handler instead of reusing one instance.
from langchain.memory import ConversationBufferMemory
from langchain.llms import OpenAI
from langchain.chains import ConversationChain
def bad_handler(user_input):
memory = ConversationBufferMemory() # fresh every call
chain = ConversationChain(llm=OpenAI(), memory=memory)
return chain.run(user_input)
print(bad_handler("My name is Ada"))
print(bad_handler("What is my name?")) # returns "I don't know"
Run this. The second call returns a generic response because memory is empty. That confirms the anti-pattern. If your real app uses LLMChain directly, the same rule applies: never construct memory inside the function that processes a message.
Step 2: Confirm memory identity across turns
Create the memory once and pass it explicitly. Log id(memory) and the buffer contents to verify the object is stable and accumulating history.
import logging
logging.basicConfig(level=logging.INFO)
memory = ConversationBufferMemory()
chain = ConversationChain(llm=OpenAI(), memory=memory)
def good_handler(user_input):
logging.info("memory id=%s buffer=%s", id(memory), memory.buffer)
return chain.run(user_input)
good_handler("My name is Ada")
good_handler("What is my name?")
If id stays constant and buffer grows, the langchain memory losing state problem is solved for in-process usage. If id changes, you’re still rebuilding it somewhere. Also inspect memory.chat_memory.messages—for buffer memory this is the canonical list of HumanMessage/AIMessage objects.
Step 3: Audit where memory is constructed
Search your codebase for Memory( and ConversationBufferMemory(. Every occurrence inside request scope is a suspect. Move instantiation to module level or a session factory keyed by user ID.
For web apps, use a dict or external store:
from langchain.memory import ConversationBufferMemory
sessions: dict[str, ConversationBufferMemory] = {}
def get_memory(session_id: str) -> ConversationBufferMemory:
if session_id not in sessions:
sessions[session_id] = ConversationBufferMemory()
return sessions[session_id]
This prevents langchain memory losing state between HTTP requests, but note that process restarts wipe the dict. In FastAPI, inject via Depends:
from fastapi import Depends
def get_chain(session_id: str):
return ConversationChain(llm=OpenAI(), memory=get_memory(session_id))
# inside route: chain = get_chain(session_id)
Step 4: Validate persistent backends
If you use RedisChatMessageHistory or SQLChatMessageHistory, the memory object may look fine in RAM but never hit the store. Subclass to log writes:
from langchain.memory import ConversationBufferMemory
from langchain.storage import RedisChatMessageHistory
class LoggingRedisHistory(RedisChatMessageHistory):
def add_user_message(self, message: str) -> None:
print(f"REDIS WRITE user: {message}")
super().add_user_message(message)
def add_ai_message(self, message: str) -> None:
print(f"REDIS WRITE ai: {message}")
super().add_ai_message(message)
memory = ConversationBufferMemory(chat_memory=LoggingRedisHistory(session_id="u1", url="redis://localhost:6379"))
Run two turns. If you see no REDIS WRITE lines, save_context isn’t being called—often because you bypassed the chain and called llm.invoke directly. Always use chain.run or chain.invoke so the memory lifecycle hooks fire. Also wrap the history in a try/except during debugging; connection errors are sometimes swallowed by higher-level handlers, causing silent langchain memory losing state.
Step 5: Instrument save_context and load
Add a temporary monkey-patch to trace every state mutation:
import langchain.memory as mem
orig_save = mem.ConversationBufferMemory.save_context
def traced_save(self, inputs, outputs):
print(f"SAVE inputs={inputs} outputs={outputs}")
return orig_save(self, inputs, outputs)
mem.ConversationBufferMemory.save_context = traced_save
orig_load = mem.ConversationBufferMemory.load_memory_variables
def traced_load(self, inputs):
vars = orig_load(self, inputs)
print(f"LOAD {vars}")
return vars
mem.ConversationBufferMemory.load_memory_variables = traced_load
Now every call that actually persists or reads state prints. If your failing path shows no SAVE but the good path does, the difference is how the chain is invoked. You can also attach a CallbackHandler to log on_chain_end for production visibility.
Step 6: Eliminate async races
With FastAPI or asyncio, concurrent requests for the same session can call load_memory_variables then save_context interleaved, causing langchain memory losing state via lost updates. Use an asyncio lock per session:
import asyncio
from langchain.memory import ConversationBufferMemory
locks: dict[str, asyncio.Lock] = {}
async def get_lock(session_id: str):
if session_id not in locks:
locks[session_id] = asyncio.Lock()
return locks[session_id]
async def async_handler(session_id: str, user_input: str, memory: ConversationBufferMemory):
lock = await get_lock(session_id)
async with lock:
chain = ConversationChain(llm=OpenAI(), memory=memory)
return await chain.arun(user_input)
Without the lock, two parallel “Hi” messages can both read empty history and overwrite each other. For threaded sync servers, use threading.Lock keyed by session. This is a classic source of intermittent langchain memory losing state that passes manual testing but fails under load.
Step 7: Write a regression test
A pytest catches langchain memory losing state before deploy. Simulate two sequential calls sharing memory.
def test_memory_persists():
from langchain.memory import ConversationBufferMemory
from langchain.llms.fake import FakeLLM
from langchain.chains import ConversationChain
memory = ConversationBufferMemory()
llm = FakeLLM(responses=["Hi Ada", "Your name is Ada"])
chain = ConversationChain(llm=llm, memory=memory)
assert chain.run("My name is Ada") == "Hi Ada"
assert "Ada" in memory.buffer
assert chain.run("What is my name?") == "Your name is Ada"
Run pytest. If it passes, state survives the chain’s internal flow. For persistent stores, point the test at a local Redis container:
def test_redis_memory_persists():
from langchain.storage import RedisChatMessageHistory
history = RedisChatMessageHistory(session_id="test", url="redis://localhost:6379")
history.clear()
history.add_user_message("My name is Ada")
assert history.messages[-1].content == "My name is Ada"
Step 8: Check memory trimming and token limits
Sometimes state isn’t lost—it’s evicted. ConversationTokenBufferMemory drops old messages when max_token_limit is exceeded. Print memory.buffer after many turns.
from langchain.memory import ConversationTokenBufferMemory
from langchain.llms import OpenAI
mem = ConversationTokenBufferMemory(llm=OpenAI(), max_token_limit=10)
mem.save_context({"input": "a"*100}, {"output": "b"*100})
print(len(mem.buffer)) # may be 0 due to trimming
If you expected those messages, increase the limit or switch to ConversationSummaryMemory. Custom memory classes that override save_context must call super().save_context or state disappears silently—another face of langchain memory losing state.
Verify success
After each step, re-run your minimal repro and the pytest. Success criteria: id(memory) is stable across turns, save_context logs appear, persistent backend receives writes, and the concurrent test with locks passes. When the bot correctly answers “Ada” on the second turn in your real app, the langchain memory losing state issue is closed. If you still see loss, capture the patched logs from Step 5 and trace which layer skips the save.