LangChain memory: how to add short-term and long-term memory

Memory in LangChain v1 has two parts. Short-term memory is the conversation thread, saved by a checkpointer that you pass to create_agent and keyed by a thread_id. Long-term memory is anything that should carry across threads, kept in a store or in an external memory service such as supermemory (LangChain docs).
The old memory classes, such as ConversationBufferMemory, are deprecated, and the table below shows what replaces each one. Everything here was checked against LangChain's docs on October 16, 2026, when langchain 1.4.6 and langgraph 1.2.15 were current.
What is conversational memory?
Conversational memory lets a chatbot use what was said earlier instead of treating every message as the first one. Models don't keep anything between calls, so your application decides what to store and what to send back with the next request.
| Type | What it keeps | How LangChain stores it | When you need it |
|---|---|---|---|
| Short-term | The current conversation | Agent state, saved by a checkpointer for each thread_id |
Any chat longer than one message |
| Long-term | Facts and preferences that matter across conversations | A store of JSON documents grouped by namespace, or an external memory service | When users come back and expect you to remember them |
Keeping a transcript in storage doesn't mean sending all of it with every request, and trimming a request doesn't have to delete the history it came from. Those are separate decisions, and the rest of this guide takes them one at a time.
How LangChain implements short-term memory
Pass a checkpointer to create_agent, then call the agent with the same thread_id to continue a conversation. This test uses a fake chat model from langchain-core, so it checks the framework's history handling without a network request:
from langchain.agents import create_agent
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
from langchain_core.messages import AIMessage
from langgraph.checkpoint.memory import InMemorySaver
model = FakeMessagesListChatModel(responses=[
AIMessage(content="Recorded for this demo."),
AIMessage(content="Second demo reply."),
])
agent = create_agent(model=model, tools=[], checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "demo-thread"}}
agent.invoke({"messages": [{"role": "user", "content": "Project: Cedar"}]}, config)
result = agent.invoke({"messages": [{"role": "user", "content": "Continue"}]}, config)
assert len(result["messages"]) == 4
assert result["messages"][0].content == "Project: Cedar"
The model returns canned text, and the assertions check that messages build up in the same thread. The example uses Python 3.12, langchain 1.4.6 and langgraph 1.2.15.
InMemorySaver keeps checkpoints in RAM, so they're gone when the process restarts. In production, use a persistent checkpointer such as PostgresSaver, and keep thread_id values under 255 characters (LangGraph persistence docs).
How do you keep a long conversation inside the context window?
A thread grows with every turn, and the whole thing is sent to the model each time. That eventually hits the model's context limit, costs more per request, and makes answers worse: LangChain's docs note that most models perform poorly over long contexts and get distracted by stale content (short-term memory docs). The two standard fixes are trimming and summarizing.
Memory with message trimming
Trimming keeps the most recent part of the thread and drops the rest before the model call. In LangChain v1 that goes in @before_model middleware:
from typing import Any
from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import before_model
from langchain.messages import RemoveMessage
from langchain_core.messages import trim_messages
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langgraph.runtime import Runtime
@before_model
def trim_history(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""Keep roughly the last 4,000 tokens of the thread."""
kept = trim_messages(
state["messages"],
max_tokens=4000,
token_counter="approximate",
strategy="last",
start_on="human", # never start on a tool result whose call was cut off
)
if len(kept) == len(state["messages"]):
return None
return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *kept]}
agent = create_agent(
model="openai:gpt-5.5",
tools=[],
middleware=[trim_history],
checkpointer=InMemorySaver(),
)
Count tokens, not messages: message sizes vary too much for a fixed count to protect the limit. start_on="human" matters once the agent uses tools, because cutting between a tool call and its result leaves a message the model can't place. The catch is that this middleware rewrites the thread's state, so whatever it drops is gone from later turns too, including the user's name from the first message.
Memory with summarization
Summarizing replaces older messages with a summary, so the thread keeps the gist without the length. LangChain ships this as middleware:
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="openai:gpt-5.5",
tools=[],
middleware=[
SummarizationMiddleware(
model="openai:gpt-5.4-mini",
trigger=("tokens", 4000), # summarize once the thread passes ~4,000 tokens
keep=("messages", 20), # keep the 20 most recent messages word for word
)
],
checkpointer=InMemorySaver(),
)
Summaries are only as good as the model and prompt that write them, and they can drop dates, negations and identifiers. Test a corrected preference and an abandoned plan: a summary that presents an old suggestion as the current decision is a memory error, however well it reads. Keep the original transcript wherever your retention policy allows, so a bad summary isn't the only copy. The context engineering guide covers budgeting the rest of the context window.
What replaced ConversationBufferMemory?
LangChain's reference marks ConversationBufferMemory and the other memory classes in the table below as deprecated since 0.3.1 and due to be removed in 2.0, and points agents that need to remember to create_agent with a checkpointer or the Store API (reference). The classes now live in the langchain-classic package, along with legacy chains such as ConversationChain (v1 migration guide).
| Old class | What it did | What to use in LangChain v1 |
|---|---|---|
ConversationBufferMemory |
Kept the whole conversation | A checkpointer on create_agent, keyed by thread_id |
ConversationBufferWindowMemory, ConversationTokenBufferMemory |
Kept the last few messages or tokens | A checkpointer plus trimming in @before_model middleware |
ConversationSummaryMemory, ConversationSummaryBufferMemory |
Kept a running summary | A checkpointer plus SummarizationMiddleware |
VectorStoreRetrieverMemory, ConversationEntityMemory |
Recalled past snippets or facts about entities | A store passed to create_agent, or an external memory service |
ConversationChain |
Wired a model to a memory object | create_agent |
How do you add long-term memory to a LangChain agent?
Long-term memory has to live outside the thread, because a new conversation starts with an empty one. You have two options.
A LangGraph store. Pass store= to create_agent, and tools can read and write it through runtime.store, under namespaces such as the user ID (long-term memory docs). You decide what to save, when, and how to find it again, and in production you back it with Postgres or another database.
A memory service. supermemory does the deciding for you: you send it conversations, and it pulls out the facts, keeps them current as they change, builds a profile for each user, and returns what's relevant to a question (how it works). In a LangChain agent, recall goes in a dynamic_prompt middleware so it runs before every model call, and the save goes after the turn. This follows the LangChain integration docs for the v5 SDK:
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, dynamic_prompt
from langgraph.checkpoint.memory import InMemorySaver
from supermemory import Supermemory # pip install supermemory (v5 SDK, 5.0.0 or later)
memory = Supermemory() # reads SUPERMEMORY_API_KEY
@dataclass
class Context:
user_id: str # from your auth, never from the request body
@dynamic_prompt
def recall_memory(request: ModelRequest) -> str:
user = request.runtime.context.user_id
question = next(m.content for m in reversed(request.messages) if m.type == "human")
profile = memory.profile(user).profile
found = memory.search(user, query=question, search_mode="memories", limit=5).results
facts = [m.memory for m in profile.static + profile.dynamic]
facts += [r.memory for r in found if r.memory]
return "You are a helpful assistant. What you know about this user:\n" + "\n".join(f"- {f}" for f in facts)
agent = create_agent(
model="openai:gpt-5.5",
tools=[],
middleware=[recall_memory],
context_schema=Context,
checkpointer=InMemorySaver(), # this thread's history
)
thread = {"configurable": {"thread_id": "chat_42"}}
question = "Can you plan my week around the launch?"
result = agent.invoke(
{"messages": [{"role": "user", "content": question}]},
thread,
context=Context(user_id="user_123"),
)
answer = result["messages"][-1].content
# Save the turn in the user's namespace. Reusing the id keeps one document per conversation.
memory.add("user_123", content=f"user: {question}\nassistant: {answer}", id="chat_42")
The user ID is the namespace, so every conversation that user has can recall the same memories, while the thread_id keeps each conversation's own history. New memories form in the background after add returns, so a fact from this turn may not reach another thread straight away; add dreaming="instant" when the next thread needs it, for one extra operation per document (v5 guide). Get an API key from the supermemory console, and see pricing for what saves and searches cost.
A few rules keep long-term memory trustworthy, whichever option you pick:
- Resolve identity on the server. The namespace or user ID should come from your authentication, not from the request body or a model's tool arguments.
- Treat a failed lookup differently from an empty one. A timeout means the search didn't run, and an empty result means nothing matched. Log them separately, or a broken memory path can look healthy for weeks.
- Save on purpose. Decide whether a user statement, a confirmed decision or a finished task triggers a save, and use a stable ID so a retried request doesn't store the same turn twice.
- Delete everywhere. If both the checkpointer and the memory service hold a copy, a deletion request has to reach both.
How should memory for a multi-session chatbot be designed?
Give each layer its own ID and its own job:
- A
thread_idper conversation for short-term memory, saved by a persistent checkpointer. - A namespace per user (or per user and workspace) for long-term memory. Never share one namespace across users, and never generate a new one per request.
- Recall before the model answers, save after. Load the user's profile and the memories that match the message, then store the finished turn.
- A context budget. Trim or summarize the thread so recalled memories, instructions and the current conversation all fit.
Three ways to build LLMs with long-term memory compares building this yourself with using a memory API, and the quickstart shows the same pattern without a framework.
Test it in two conversations
Test the framework and the memory separately. For the framework: messages build up in the same thread, a different thread starts empty, history survives a process restart with your durable checkpointer, and tool calls stay paired with their results after trimming. For the memory: start a second thread for the same user and check that a preference from the first one is recalled, start one for a different user and check that it isn't, then correct the preference and check that the new version wins.
Record the package versions, model, prompts and data with the results, so you can reproduce a failure after an upgrade.
Common questions
Does LangChain have memory?
Yes. Short-term memory is built in: give create_agent a checkpointer and a thread_id and it keeps the conversation. Long-term memory across conversations needs a store or an external memory service.
What's the difference between short-term and long-term memory in LangChain?
Short-term memory is one conversation thread, saved by a checkpointer. Long-term memory is information shared across threads, such as a user's preferences, saved in a store or a memory service under a namespace like the user ID.
Is ConversationBufferMemory deprecated?
Yes. It has been deprecated since langchain 0.3.1, now lives in langchain-classic, and is due to be removed in 2.0. Use create_agent with a checkpointer instead.
Do I need LangGraph to use LangChain memory?
Agents built with create_agent run on LangGraph, and the checkpointers and stores come from LangGraph packages, so you're already using it. For graphs you build yourself, the LangGraph memory guide covers checkpointers, stores and restarts in more depth.