0Pricing
AI Engineering Academy · Lesson

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 dropped

Implementing 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 results

Combining 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 window

Buffer 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 history

Monitoring 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
        pass

Quick 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.

All lessons in this course

  1. Why Stateless LLMs Need External Memory
  2. Buffer and Window Memory
  3. Summary Memory and Token-Aware Truncation
  4. Persisting Chat History in Redis and PostgreSQL
← Back to AI Engineering Academy