Buffer and Window Memory
Implement ConversationBufferMemory and ConversationBufferWindowMemory to keep the last N turns in context, and measure how window size affects coherence and cost.
Buffer and Window Memory is a free AI Engineering Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Buffer Memory: Keep Everything
Buffer memory is the simplest strategy: store every message from every turn in a list and include the complete history in every API call. It preserves perfect context — the model can reference anything said at any point. The downside is linear context growth with no bound. For short sessions like a single task completion, buffer memory is perfectly appropriate and the easiest to implement.
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(
return_messages=True, # return Message objects, not a string
memory_key='history' # key to inject into prompt template
)
# Add messages manually
memory.chat_memory.add_user_message('What is a neural network?')
memory.chat_memory.add_ai_message('A neural network is a system of layers...')
# Load what will be injected
print(memory.load_memory_variables({}))Integrating Buffer Memory with a Chain
To use buffer memory with LCEL, wire it through RunnableWithMessageHistory or use ConversationChain for the legacy approach. The memory object holds the history and the chain uses a MessagesPlaceholder in the prompt template to inject it. After each invocation, the memory automatically appends the new turn.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_history(session_id: str):
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
chain = (
ChatPromptTemplate.from_messages([
('system', 'You are helpful.'),
MessagesPlaceholder('history'),
('human', '{input}')
])
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
with_memory = RunnableWithMessageHistory(
chain, get_history,
input_messages_key='input',
history_messages_key='history'
)The Problem with Unlimited Buffer
Buffer memory works until the conversation grows too long and exceeds the model's context window. With GPT-4o-mini's 128K context, a typical chat might last 200–400 turns before overflowing. More practically, even at 50 turns, you are sending 50K+ tokens per request — a significant cost. You need a strategy to bound the history size.
import tiktoken
enc = tiktoken.encoding_for_model('gpt-4o-mini')
def count_history_tokens(messages: list) -> int:
total = 0
for msg in messages:
total += len(enc.encode(msg.content))
total += 4 # per-message overhead
return total
# Check how big the history has grown
history = store.get('session-1')
if history:
token_count = count_history_tokens(history.messages)
print(f'History size: {len(history.messages)} messages, {token_count} tokens')Window Memory: Keep Last N Turns
Window memory keeps only the last K conversation turns, discarding older messages. This bounds the context at K * avg_tokens_per_turn regardless of how long the conversation lasts. The trade-off is that very early context is lost — the model may forget things the user said many turns ago. For most general-purpose chatbots, a window of 5–10 turns provides good coherence at acceptable cost.
from langchain.memory import ConversationBufferWindowMemory
# Keep last 5 turns (10 messages: 5 user + 5 assistant)
memory = ConversationBufferWindowMemory(
k=5, # number of TURNS to keep (each turn = user + AI)
return_messages=True,
memory_key='history'
)
# After 10 turns, only turns 6-10 will be in context
# Turns 1-5 are silently droppedImplementing Window Memory with InMemoryChatMessageHistory
LangChain's InMemoryChatMessageHistory keeps all messages, but you can trim the history before injecting it into the prompt. A common pattern is to store the full history for logging but pass only the last N messages to the LLM. Use Python slice notation on history.messages to get the most recent window.
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables import RunnableLambda
WINDOW_SIZE = 10 # last 10 messages (5 turns)
def get_windowed_history(session_id: str):
full_history = store.get(session_id, InMemoryChatMessageHistory())
store[session_id] = full_history
return full_history
# In the chain, trim before injecting
def trim_history(messages):
return messages[-WINDOW_SIZE:] if len(messages) > WINDOW_SIZE else messages
# Use in prompt with trimming
from langchain_core.messages import trim_messages
trimmer = trim_messages(
max_tokens=2000,
strategy='last',
token_counter=ChatOpenAI(model='gpt-4o-mini'),
include_system=True
)Token-Based vs Turn-Based Window
Window size can be specified in turns (last K human/AI pairs) or in tokens (last T tokens of history). Token-based windowing is more reliable because turn length varies — a turn with code is 10x longer than a turn with 'yes'. LangChain's trim_messages() utility supports both strategies and can preserve the system prompt while trimming history.
from langchain_core.messages import trim_messages, SystemMessage, HumanMessage, AIMessage
# Token-based trimming — keep last 1000 tokens of conversation
trimmer = trim_messages(
max_tokens=1000,
strategy='last', # keep most recent messages
token_counter=len, # approximate: count characters / 4
include_system=True, # always include the system prompt
allow_partial=False, # don't split a message in half
start_on='human' # start window on a human message
)
trimmed = trimmer.invoke(all_messages)
print(f'Trimmed to {len(trimmed)} messages')Measuring Coherence vs Window Size
Choosing the right window size requires measuring how conversation coherence degrades as window size shrinks. Run a set of test conversations where turn N references information from turn N-5, N-10, and N-20. Test whether the model can still answer correctly with windows of 5, 10, and 20 turns. The answer gives you the minimum window size needed for your specific use case.
def test_reference_at_distance(chain_with_memory, distances=[5, 10, 20]):
results = {}
for distance in distances:
session_id = f'test-dist-{distance}'
# Fill with filler turns
for i in range(distance):
chain_with_memory.invoke(
{'input': f'Turn {i}: filler message'},
config={'configurable': {'session_id': session_id}}
)
# Ask about something said at the start
first_msg = 'Recall that the user said the magic word is AZURE.'
response = chain_with_memory.invoke(
{'input': 'What was the magic word?'},
config={'configurable': {'session_id': session_id}}
)
results[distance] = 'AZURE' in response.upper()
return resultsCombining System Prompt with Window Memory
When using window memory, always preserve the system prompt — it defines the AI persona and rules. Without it, the model may lose its persona after the window slides past the first turn. Use the include_system=True option in trim functions, or always insert the system message before the windowed history in your prompt template.
# Always put system prompt BEFORE the windowed history
prompt = ChatPromptTemplate.from_messages([
('system', 'You are a Python tutor. Always explain with code examples.'),
MessagesPlaceholder('history'), # windowed history injected here
('human', '{input}'),
])
# The system prompt is never trimmed — only the history window is managed
# This ensures the model's persona is always present regardless of windowBuffer vs Window Memory Decision Guide
Use buffer memory when: conversations are short and bounded (a one-time task, a form wizard), full context is critical (legal analysis, code review), and token budget is not a concern. Use window memory when: conversations can be arbitrarily long (customer support, general assistants), recent context is more important than distant context, and you need predictable, bounded token costs.
# Decision matrix in code
def choose_memory_strategy(
expected_turns: int,
max_context_tokens: int = 128000,
avg_tokens_per_turn: int = 200
) -> str:
buffer_tokens = expected_turns * avg_tokens_per_turn
if buffer_tokens < max_context_tokens * 0.5:
return 'buffer' # Safe to keep everything
elif expected_turns <= 20:
return 'window_10' # Keep last 10 turns
else:
return 'summary' # Need summarization for long convos
print(choose_memory_strategy(5)) # 'buffer'
print(choose_memory_strategy(50)) # 'window_10'
print(choose_memory_strategy(200)) # 'summary'Persisting the Window to Redis
For production use, store the full conversation history in Redis (for fast retrieval) and trim it to the window size at read time. Redis with a TTL ensures old sessions expire automatically. Use a sorted set or a list with LRANGE to efficiently fetch only the last N messages without loading the entire history.
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_windowed_redis_history(session_id: str, window: int = 10):
# RedisChatMessageHistory stores all messages
history = RedisChatMessageHistory(
session_id=session_id,
url='redis://localhost:6379',
ttl=3600 # 1 hour TTL
)
# Trim to window size in-memory before use
all_msgs = history.messages
if len(all_msgs) > window * 2: # window turns = window*2 messages
history.messages = all_msgs[-(window * 2):]
return historyMonitoring Memory Health in Production
Track these metrics in production to catch memory-related issues: average history tokens per request to detect sessions growing too large, context window utilization (warn if > 80%), session count in store to detect memory leaks in the session store, and cache hit rate for sessions reloaded from Redis. Alert when average history tokens crosses a configurable threshold.
import time
from langchain_core.callbacks import BaseCallbackHandler
class MemoryMonitorCallback(BaseCallbackHandler):
def on_chain_start(self, serialized, inputs, **kwargs):
history = inputs.get('history', [])
token_estimate = sum(len(m.content.split()) * 1.3 for m in history)
if token_estimate > 50000:
print(f'WARNING: Large history {token_estimate:.0f} estimated tokens')
def on_chain_end(self, outputs, **kwargs):
# Log usage for dashboards
passQuick Check
Test your understanding of buffer and window memory strategies.
Lesson Recap
In this lesson you learned: buffer memory preserves complete history but grows without bound, making it suitable only for short bounded conversations, window memory keeps only the last K turns for predictable bounded cost at the expense of distant context, and token-based trimming with trim_messages() is more reliable than turn-based windows because message length varies. Next up we explore summary memory for open-ended long conversations.
Frequently asked questions
Is the “Buffer and Window Memory” lesson free?
Yes — the full text of “Buffer and Window Memory” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “Buffer and Window Memory”?
Implement ConversationBufferMemory and ConversationBufferWindowMemory to keep the last N turns in context, and measure how window size affects coherence and cost. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Buffer and Window Memory” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.