LangChain RunnableWithMessageHistory — session_id and config_specs errors
<code>RunnableWithMessageHistory</code> is the modern LCEL-native replacement for legacy memory classes. Its per-session routing and config_specs are more powerful but require correct wiring.
Quick fix (TL;DR)
RunnableWithMessageHistory wraps any chain and injects prior messages based on a session_id looked up at invoke time. Fix by (a) passing config={"configurable": {"session_id": "..."}} on every invoke, (b) implementing a get_session_history(session_id) -> BaseChatMessageHistory callback, (c) defining input_messages_key and history_messages_key for chat prompts, and (d) using persistent history (SQL / Redis) in production, not in-memory.Real error messages you'll see
These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
ValueError: session_id must be provided in the config. Pass config={"configurable": {"session_id": "..."}} when invoking.KeyError: 'question'
# input_messages_key="question" but the prompt has {input}, not {question}# turn 1: user says "My name is Alice" # turn 2: user says "What is my name?" # response: "I do not know your name." # ← history_messages_key does not match the prompt placeholder
Reference
RunnableWithMessageHistory required wiring
| Parameter | What it does |
|---|---|
runnable | The base chain to wrap |
get_session_history | Callable that returns history for a given session_id |
input_messages_key | Key on the input dict containing the user message |
history_messages_key | Key on the input dict where history is injected |
output_messages_key | Where to find the AI response for saving (multi-output chains) |
Common chat history backend choices
| Backend | Package | Persistence |
|---|---|---|
InMemoryChatMessageHistory | langchain-core | Process memory only |
SQLChatMessageHistory | langchain-community | Any SQLAlchemy DB |
RedisChatMessageHistory | langchain-community | Redis |
MongoDBChatMessageHistory | langchain-mongodb | MongoDB |
PostgresChatMessageHistory | langchain-postgres | PostgreSQL |
DynamoDBChatMessageHistory | langchain-aws | DynamoDB |
Root causes, ranked by frequency
Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.
- 26%session_id not passed at invoke time. Config missing or malformed.
- 18%input_messages_key or history_messages_key wrong. Names must match the wrapped prompt/chain expectations.
- 14%Using in-memory history in production. Dies with the process; history lost across restarts and workers.
- 10%Multiple workers not sharing history. Different processes have different in-memory stores; user's history depends on which worker served the request.
- 8%Prompt has no MessagesPlaceholder for history. History is injected under a key but the prompt does not reference it.
- 7%Async chain wrapped in a sync history. Sync SQL / Redis clients block the event loop.
- 7%Session_id includes user input. Sanitize — commas, spaces, and special chars break routing in some backends.
- 10%History object retained across sessions. Cached history reference bleeds messages between users.
Fixes — copy-paste solutions
Wire the four required pieces correctly
The wrapper needs to know: what chain to run, how to load history for a session, where to find the new user message in the input, and where to inject the history in the prompt.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.messages import BaseMessage from typing import Dict # 1) The base chain prompt = ChatPromptTemplate.from_messages([ ("system", "You are a helpful assistant. Refer to prior turns when relevant."), MessagesPlaceholder(variable_name="history"), # ← history goes here ("human", "{input}"), # ← new user message here ]) model = ChatOpenAI(model="gpt-4o-mini") chain = prompt | model | StrOutputParser() # 2) The session-history store (in-memory for demo; use SQL/Redis in prod) store: Dict[str, InMemoryChatMessageHistory] = {} def get_session_history(session_id: str) -> InMemoryChatMessageHistory: if session_id not in store: store[session_id] = InMemoryChatMessageHistory() return store[session_id] # 3) Wire the wrapper chain_with_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="input", # matches {input} in the prompt history_messages_key="history", # matches MessagesPlaceholder variable_name ) # 4) Invoke with session_id config = {"configurable": {"session_id": "user_42"}} r1 = chain_with_history.invoke({"input": "My name is Alice."}, config=config) print(r1) # > Nice to meet you, Alice! r2 = chain_with_history.invoke({"input": "What is my name?"}, config=config) print(r2) # > Your name is Alice.
_key parameters are the source of most config errors. Trace them from the prompt: whatever your prompt uses (e.g. MessagesPlaceholder("history")) must match history_messages_key="history".Use persistent history for production (SQL, Redis, or DynamoDB)
Every real app needs history that survives process restarts and works across workers. Pick a backend and wire it into get_session_history.
from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.chat_history import BaseChatMessageHistory # Option A: SQL (SQLite, Postgres, MySQL — via SQLAlchemy) def get_sql_history(session_id: str) -> BaseChatMessageHistory: return SQLChatMessageHistory( session_id=session_id, connection="postgresql+psycopg2://user:pass@localhost/mydb", # The table is auto-created on first use table_name="chat_history", ) # Option B: Redis from langchain_community.chat_message_histories import RedisChatMessageHistory def get_redis_history(session_id: str) -> BaseChatMessageHistory: return RedisChatMessageHistory( session_id=session_id, url="redis://localhost:6379/0", ttl=60 * 60 * 24 * 7, # 7-day retention on Redis ) # Option C: DynamoDB (for AWS-native apps) from langchain_aws.chat_message_histories import DynamoDBChatMessageHistory def get_dynamo_history(session_id: str) -> BaseChatMessageHistory: return DynamoDBChatMessageHistory( table_name="chat_history_prod", session_id=session_id, ) # Wire whichever you chose chain_with_history = RunnableWithMessageHistory( chain, get_sql_history, # or get_redis_history, get_dynamo_history input_messages_key="input", history_messages_key="history", ) # Async version — use SQL with async driver or the async-friendly backends # (make sure to use the async variant of the history class if your chain is async)
PostgresChatMessageHistory from langchain-postgres — it has proper async support. The community SQL variant may block the event loop.Add per-user + per-conversation config_specs for finer routing
By default only session_id is required. Add history_factory_config to require additional config fields (e.g. user_id) that flow to your history factory.
from langchain_core.runnables import ConfigurableFieldSpec from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_community.chat_message_histories import SQLChatMessageHistory def get_session_history(user_id: str, conversation_id: str): """Look up history scoped to both user and conversation.""" return SQLChatMessageHistory( session_id=f"{user_id}:{conversation_id}", connection="postgresql+psycopg2://...", ) chain_with_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="input", history_messages_key="history", history_factory_config=[ ConfigurableFieldSpec( id="user_id", annotation=str, name="User ID", description="The current user's ID", default="", is_shared=True, ), ConfigurableFieldSpec( id="conversation_id", annotation=str, name="Conversation ID", description="This conversation's ID within the user's history", default="", is_shared=True, ), ], ) # Invoke with the extra config config = { "configurable": { "user_id": "alice_42", "conversation_id": "conv_2026_07_30_001", } } response = chain_with_history.invoke({"input": "Hello"}, config=config)
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Always pass
config={"configurable": {"session_id": "..."}}at invoke time. - Trace
input_messages_keyandhistory_messages_keyto the prompt template. - Use persistent history in production — SQL, Redis, or DynamoDB. Never in-memory.
- For multi-tenant apps, use
history_factory_configwith user_id + conversation_id. - Sanitize session_ids — no arbitrary user input directly as an ID.
- For async chains, use async-friendly history backends to avoid blocking the event loop.
- Set TTL on history storage — chat data should not accumulate forever.
Frequently asked questions
RunnableWithMessageHistory wrapping any LCEL chain, with a chosen persistence backend. The old memory classes are deprecated and will be removed. See our migration page for the exact rewrite.trim_messages from langchain-core before injection; (2) trim at write time in your history backend. Prompt-time trimming is safer because it keeps the raw history intact for future changes.invoke and ainvoke work. Make sure your history backend has an async variant when you use ainvoke to avoid blocking.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.