What does RunnableWithMessageHistory fix that the old ConversationBufferMemory got wrong?
RunnableWithMessageHistory parameterises history by session_id with a per-call factory, fixing the statefulness bug in ConversationBufferMemory that made it unsafe to share across concurrent sessions.
Picture a notebook on the wall labelled 'last conversation'. If two people use the same notebook at the same time, they bleed into each other's notes. The fix is to give each person a notebook with their name on it and have a clerk hand the right notebook to whoever walks in. The clerk is the per-session lookup; the named notebook is the session-keyed history. The same shared chain can now serve everyone safely.
Detailed answer & concept explanation~5 min readEverything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example.
Everything important, quickly.
3 minutes: name the statefulness bug in old memory, describe the session_id parameterization fix, walk the get_session_history factory pattern, and connect to LangGraph's checkpointer as the next-step generalization.
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
store: dict[str, BaseChatMessageHistory] = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory() # swap for Redis / Postgres in prod
return store[session_id]
prompt = ChatPromptTemplate.from_messages([
('system', 'You are a helpful assistant.'),
MessagesPlaceholder(variable_name='history'),
('human', '{input}'),
])
chain = prompt | ChatOpenAI(model='gpt-4o-mini')
chat = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key='input',
history_messages_key='history',
)
# Same `chat` object: different sessions, isolated history
chat.invoke({'input': 'Hi, my name is Alex.'},
config={'configurable': {'session_id': 'alex'}})
chat.invoke({'input': 'Who am I?'},
config={'configurable': {'session_id': 'alex'}}) # remembers
chat.invoke({'input': 'Who am I?'},
config={'configurable': {'session_id': 'beth'}}) # cold startReal products, models, and research that use this idea.
- LangChain's current chatbot tutorials use RunnableWithMessageHistory with Redis backing
- Production deployments serving Claude Opus 4.7 or GPT-5.5 chat use this pattern with Postgres-backed history for durability
- Vercel AI SDK takes a different approach, history is passed in per request explicitly, but the structural principle (no state on the shared chain) is the same
- Many production teams migrate further from RunnableWithMessageHistory to LangGraph once they need typed state beyond message history
What an interviewer would ask next. Try answering before peeking at the approach.
QHow does this pattern compare to LangGraph's checkpointer for managing conversation state?
QWhat goes wrong if get_session_history returns a new in-memory history every call?
Don't say thisRed flags and common mistakes that signal junior thinking. Click to expand.
Red flags and common mistakes that signal junior thinking. Click to expand.
Reading the question as being about a feature (compression, windowing, encryption) when the actual fix is structural. Statefulness was the bug, not buffer size or security.
The night-before-the-interview bullets. Scan these on the way to the call.
Primary sources. Skim if you want the original framing.
Same topic, related formats. Practice these next.