Zenaique

Why did LangChain replace `ConversationBufferMemory` with `RunnableWithMessageHistory`?

Flashcard·Medium·4.0 · 0·~30s·Asked atIroncladKpmgLambda Labs
Attempt it
TL;DR

Old `Memory` held state on the chain object, breaking multi-tenancy; `RunnableWithMessageHistory` parameterises history by `session_id` and keeps the chain stateless.

Memory aid
Sign in to see the mnemonic that makes this stick.
Easy to grasp

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.

Key concepts

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.

How the breakage manifested in production
The new contract: externalize state, key by session
Three production wins from the new pattern
Where this fits relative to LangGraph
Sign in to unlock the full deep dive.

Situations where this technique stops working.

Sign in to see when this approach fails.

2–4 min · Everything important, quickly.

Sign in to see the quick scan of the deep dive.

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.
Sign in to see more production examples.

What an interviewer would ask next. Try answering before peeking at the approach.

QHow would you implement `ConversationSummaryMemory`'s semantics on top of `RunnableWithMessageHistory`?
A

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.

2 more follow-ups an interviewer would ask next. Sign in to reveal them.

Red flags & common mistakes

The phrases that signal junior thinking. Click to expand.

Most common mistake

Sharing a chain across users when the chain has a stateful Memory attached, two users now silently share each other's conversation history.

Sign in to see all red flags and common mistakes.

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.

Sign in to unlock the revision sheet.

Primary sources. Browse if you want the original framing.

Similar questions

Same topic, related formats. Practice these next.

4 curated
Next question
Defend the call to…
Short answer·Hard