Why did LangChain replace `ConversationBufferMemory` with `RunnableWithMessageHistory`?
Old `Memory` held state on the chain object, breaking multi-tenancy; `RunnableWithMessageHistory` parameterises history by `session_id` and keeps the chain stateless.
Picture a customer-service phone line where every caller hears the previous caller's last sentence whispered into their ear because the operator never resets her notepad. That's the old `ConversationBufferMemory`, one shared notepad on the chain object. The new pattern hands every caller their own labeled folder; the operator pulls the right folder for the right caller, reads from it, writes to it, and puts it back. The phone line itself stays clean, each caller's history lives in their own folder addressed by a session id.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
LangChain's deprecation of ConversationBufferMemory is one of the better-motivated breaking changes in the library's history. The old pattern looked fine in tutorials, ran fine in notebooks, and quietly broke when production traffic arrived. The new pattern, RunnableWithMessageHistory, is more verbose for the trivial case but aligns memory with the stateless Runnable model that the rest of LangChain depends on.
This explanation walks the structural mismatch, the concrete production failure mode, the new contract, and the relationship to LangGraph's checkpointer-based memory for agent graphs.
The structural mismatch that prompted the deprecation
Runnables are designed to be stateless. The contract is: invoke a Runnable with an input, get an output that's a deterministic function of that input (modulo provider-side non-determinism in LLM calls). The whole reason prompt | model | parser works is that each step's behavior depends on its input, not on what other calls happened to be in flight or completed earlier. That's what lets you share a chain across requests, instantiate it once at module load, and trust that two concurrent calls don't interfere.
ConversationBufferMemory violated this. It held the conversation history as an attribute on the object (self.buffer) and appended to it on every call. Attach the memory to a chain, and the chain now has hidden state: its behavior depends on what previous calls left in the buffer.
In a notebook with one user, this is fine and even convenient. In any production service with concurrent users, it's a multi-tenancy bug waiting to happen. The framework gave you a primitive (stateless Runnables) and a feature (Memory) whose semantics directly contradicted each other.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
Real products, models, and research that use this idea.
- Production LangChain apps using `RedisChatMessageHistory` keyed by user-session id, with TTL configured at the Redis layer for automatic cleanup.
- LangChain's own migration docs show the before/after pattern explicitly, deprecating `ConversationBufferMemory` and showing `RunnableWithMessageHistory` as the replacement.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you implement `ConversationSummaryMemory`'s semantics on top of `RunnableWithMessageHistory`?
Two clean approaches. One: subclass BaseChatMessageHistory with a custom add_messages that triggers a summarization LLM call when the buffer exceeds a token threshold, replacing old turns with a summary message. Two: keep raw history in the store and add a chain step before the prompt that conditionally summarizes if {history} is too long. Option two keeps the storage layer simple and the compression behavior explicit.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Sharing a chain across users when the chain has a stateful Memory attached, two users now silently share each other's conversation history.
60 second bullets to scan on the way to the call.
Why a stateful Memory attached to a stateless Runnable breaks multi-tenancy.
How RunnableWithMessageHistory externalises history into a store.
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.