LangChain RunnableWithMessageHistory — config_specs and session_id routing errors (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain Message history config
LangChain Memory · Message History Severity: Medium HTTP n/a

LangChain RunnableWithMessageHistorysession_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.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: 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.

Missing session_id at invoke
ValueError: session_id must be provided in the config. Pass config={"configurable": {"session_id": "..."}} when invoking.
input_messages_key not matching prompt variable
KeyError: 'question'
# input_messages_key="question" but the prompt has {input}, not {question}
History not injected — chain replies as fresh session
# 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

ParameterWhat it does
runnableThe base chain to wrap
get_session_historyCallable that returns history for a given session_id
input_messages_keyKey on the input dict containing the user message
history_messages_keyKey on the input dict where history is injected
output_messages_keyWhere to find the AI response for saving (multi-output chains)

Common chat history backend choices

BackendPackagePersistence
InMemoryChatMessageHistorylangchain-coreProcess memory only
SQLChatMessageHistorylangchain-communityAny SQLAlchemy DB
RedisChatMessageHistorylangchain-communityRedis
MongoDBChatMessageHistorylangchain-mongodbMongoDB
PostgresChatMessageHistorylangchain-postgresPostgreSQL
DynamoDBChatMessageHistorylangchain-awsDynamoDB

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

Fix #1

Wire the four required pieces correctly

Chain + history factory + input key + history key.

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.

basic_message_history.py
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.
The two _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".
Fix #2

Use persistent history for production (SQL, Redis, or DynamoDB)

In-memory history is fine for demos and unit tests only.

Every real app needs history that survives process restarts and works across workers. Pick a backend and wire it into get_session_history.

persistent_history.py
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)
For Postgres in async apps, use PostgresChatMessageHistory from langchain-postgres — it has proper async support. The community SQL variant may block the event loop.
Fix #3

Add per-user + per-conversation config_specs for finer routing

For multi-tenant apps that need both user_id and conversation_id.

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.

multi_tenant_history.py
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)
Multi-tenant apps almost always want user + conversation scoping. Do not use a single flat session_id — it makes cross-user contamination possible if a bug causes ID collisions.

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_key and history_messages_key to the prompt template.
  • Use persistent history in production — SQL, Redis, or DynamoDB. Never in-memory.
  • For multi-tenant apps, use history_factory_config with 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.
For sync backends, yes. For async backends (Postgres async, Redis async), it does not block the event loop but still takes measurable time. Consider caching the history object at the request-handler level for a chain with multiple invokes per request.
Two options: (1) trim in the prompt with 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.
Yes — implement a custom history class that runs a summarization prompt when the buffer exceeds N messages, then stores the summary + recent messages. LangChain does not ship this by default but the pattern is documented in cookbook examples.
Yes — both invoke and ainvoke work. Make sure your history backend has an async variant when you use ainvoke to avoid blocking.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.