Setup and context — Why Long-Term Memory Matters
Building a basic chat with Gemini API's startChat() is straightforward. But production chatbots face challenges that go far beyond simple back-and-forth: conversations spanning dozens of turns, per-user session management, conversation resumption after server restarts, and graceful handling of infrastructure failures.
While Gemini 2.5 Pro boasts a context window of up to 1 million tokens, sending the entire conversation history with every request is neither cost-effective nor practical from a latency perspective. This article presents battle-tested design patterns for long-term memory and session persistence that actually work in production, complete with implementation code.
This guide is intended for intermediate to advanced developers who already understand basic Gemini API chat implementation. For foundational multi-turn chat concepts, see [Getting Started with Gemini API Multi-Turn Chat]((/articles/gemini-api/gemini-multi-turn-chat-guide).
The Three-Layer Memory Architecture
For production chatbot memory management, a three-layer architecture has proven to be both practical and effective.
Layer 1: Working Memory (Recent Conversation)
The most recent N turns (typically 5–10) are kept verbatim. This is essential for immediate responsiveness when users reference something they "just said."
Layer 2: Summary Memory (Compressed History)
Older turns pushed out of working memory are summarized and compressed using Gemini itself. This condenses hundreds of turns into a few hundred tokens, preserving long-term context while dramatically reducing token costs.
Layer 3: Fact Memory (Extracted Persistent Facts)
Persistent facts about the user (name, preferences, past decisions) are extracted from conversations and stored as structured data. These survive across sessions and form the foundation for personalization.
# Core memory manager structure
import json
from google import genai
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
class ConversationMemory:
"""Three-layer memory architecture implementation"""
def __init__(self, working_memory_limit=10):
self.working_memory = [] # Layer 1: recent turns
self.summary = "" # Layer 2: compressed history
self.facts = {} # Layer 3: persistent facts
self.working_memory_limit = working_memory_limit
self.total_turns = 0
def add_turn(self, role: str, content: str):
"""Add a conversation turn and compress memory if needed"""
self.working_memory.append({"role": role, "parts": [{"text": content}]})
self.total_turns += 1
# Compress when working memory exceeds the limit
if len(self.working_memory) > self.working_memory_limit:
self._compress_memory()
def _compress_memory(self):
"""Compress older turns into a summary"""
# Extract the oldest half for summarization
half = len(self.working_memory) // 2
old_turns = self.working_memory[:half]
self.working_memory = self.working_memory[half:]
old_text = "\n".join(
f"{t['role']}: {t['parts'][0]['text']}" for t in old_turns
)
# Use Gemini to generate the summary
prompt = f"""Summarize the following conversation history in 3-5 sentences,
preserving all important information. If there is an existing summary,
integrate it with the new content.
Existing summary:
{self.summary if self.summary else "(none)"}
New conversation:
{old_text}
Integrated summary:"""
response = client.models.generate_content(
model="gemini-2.5-flash", # Use Flash for cost efficiency
contents=prompt
)
self.summary = response.text
def build_context(self) -> list:
"""Build the context to send to the API"""
context = []
# Prepend summary as background context
if self.summary:
context.append({
"role": "user",
"parts": [{"text": f"[Background context] {self.summary}"}]
})
context.append({
"role": "model",
"parts": [{"text": "Understood. I'll keep this context in mind."}]
})
# Add fact memory if available
if self.facts:
facts_text = "About the user: " + ", ".join(
f"{k}: {v}" for k, v in self.facts.items()
)
context.append({
"role": "user",
"parts": [{"text": f"[User information] {facts_text}"}]
})
context.append({
"role": "model",
"parts": [{"text": "Noted. I have the user's information."}]
})
# Append working memory
context.extend(self.working_memory)
return contextWith this architecture, even conversations exceeding 100 turns maintain a bounded token count per API request.
Token Budget Control — Optimizing Cost and Latency
In production, you need strict control over the number of tokens per request. Here's how to implement budget management using Gemini API's count_tokens() method.
class TokenBudgetManager:
"""Manages token budgets and optimizes context window usage"""
def __init__(
self,
model_name: str = "gemini-2.5-pro",
max_input_tokens: int = 30000, # Input token ceiling
reserved_output_tokens: int = 4000 # Reserved for output
):
self.model_name = model_name
self.max_input_tokens = max_input_tokens
self.reserved_output_tokens = reserved_output_tokens
self.client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
def count_tokens(self, contents: list) -> int:
"""Count tokens in the given content"""
response = self.client.models.count_tokens(
model=self.model_name,
contents=contents
)
return response.total_tokens
def fit_to_budget(self, memory: ConversationMemory) -> list:
"""Ensure memory contents fit within the token budget"""
context = memory.build_context()
token_count = self.count_tokens(context)
# Return as-is if within budget
if token_count <= self.max_input_tokens:
return context
# Over budget: progressively compress working memory
while token_count > self.max_input_tokens and len(memory.working_memory) > 2:
memory._compress_memory()
context = memory.build_context()
token_count = self.count_tokens(context)
return context
def estimate_cost(self, input_tokens: int, output_tokens: int) -> dict:
"""Calculate estimated cost in USD
Note: prices are approximate as of March 2026"""
# Gemini 2.5 Pro pricing (reference values)
input_cost = (input_tokens / 1_000_000) * 1.25
output_cost = (output_tokens / 1_000_000) * 10.00
return {
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"input_cost_usd": round(input_cost, 6),
"output_cost_usd": round(output_cost, 6),
"total_cost_usd": round(input_cost + output_cost, 6)
}Token Budget Design Guidelines
Token budget decisions involve balancing cost, latency, and conversation quality. Here are practical guidelines:
- 30,000 token input ceiling: Sufficient context for general-purpose chatbots. Good balance between cost and response speed
- 100,000 token input ceiling: For use cases requiring deep context, such as document analysis or specialized advisory applications
- 2,000–8,000 reserved output tokens: Adjust based on expected response length. 2,000 for FAQ bots, 8,000 for coding assistants
- Compression trigger: Start summarization when working memory exceeds 60% of the input ceiling
Session Persistence — Redis and Cloudflare KV Implementations
To maintain conversations across server restarts and scale-out events, session data must be persisted to external storage.
Redis-Based Session Persistence
import redis
import json
import time
class RedisSessionStore:
"""Redis-based session persistence"""
def __init__(self, redis_url: str = "redis://localhost:6379", ttl: int = 86400):
self.redis = redis.from_url(redis_url)
self.ttl = ttl # Session expiry (default: 24 hours)
def save_session(self, session_id: str, memory: ConversationMemory):
"""Save session to Redis"""
data = {
"working_memory": memory.working_memory,
"summary": memory.summary,
"facts": memory.facts,
"total_turns": memory.total_turns,
"updated_at": time.time()
}
self.redis.setex(
f"chat:session:{session_id}",
self.ttl,
json.dumps(data, ensure_ascii=False)
)
def load_session(self, session_id: str) -> ConversationMemory | None:
"""Restore session from Redis"""
raw = self.redis.get(f"chat:session:{session_id}")
if raw is None:
return None
data = json.loads(raw)
memory = ConversationMemory()
memory.working_memory = data["working_memory"]
memory.summary = data["summary"]
memory.facts = data.get("facts", {})
memory.total_turns = data.get("total_turns", 0)
return memory
def delete_session(self, session_id: str):
"""Delete a session"""
self.redis.delete(f"chat:session:{session_id}")
def extend_ttl(self, session_id: str):
"""Extend TTL for active sessions"""
self.redis.expire(f"chat:session:{session_id}", self.ttl)Cloudflare KV Session Persistence (Edge-Ready)
When running chatbots on Cloudflare Workers, KV provides a natural storage layer.
// Cloudflare Workers + KV session management
interface SessionData {
workingMemory: Array<{ role: string; parts: Array<{ text: string }> }>;
summary: string;
facts: Record<string, string>;
totalTurns: number;
updatedAt: number;
}
export class KVSessionStore {
constructor(private kv: KVNamespace, private ttlSeconds: number = 86400) {}
async saveSession(sessionId: string, data: SessionData): Promise<void> {
await this.kv.put(
`chat:session:${sessionId}`,
JSON.stringify(data),
{ expirationTtl: this.ttlSeconds }
);
}
async loadSession(sessionId: string): Promise<SessionData | null> {
const raw = await this.kv.get(`chat:session:${sessionId}`);
if (!raw) return null;
return JSON.parse(raw) as SessionData;
}
async deleteSession(sessionId: string): Promise<void> {
await this.kv.delete(`chat:session:${sessionId}`);
}
}
// Usage in a Worker entry point
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const store = new KVSessionStore(env.CHAT_SESSIONS);
const sessionId = getCookieValue(request, "session_id");
// Restore session
const existing = await store.loadSession(sessionId);
// ... Gemini API call ...
// Save updated session
await store.saveSession(sessionId, updatedData);
return new Response(JSON.stringify(result), {
headers: { "Content-Type": "application/json" }
});
}
};Fact Extraction — Automatically Learning User Context
Building an effective Layer 3 (fact memory) requires a mechanism to automatically extract persistent facts about the user from conversations.
class FactExtractor:
"""Extract persistent user facts from conversations"""
EXTRACTION_PROMPT = """Extract persistent facts about the user from the
conversation below in JSON format. Do not include temporary topics.
Target information:
- Name, role, organization
- Technical skills, programming languages
- Project goals and constraints
- Explicitly stated preferences
Existing facts (update and supplement):
{existing_facts}
Conversation:
{conversation}
Output as JSON:
{{"name": "...", "role": "...", "skills": ["..."], ...}}"""
def __init__(self):
self.client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
def extract(
self,
conversation: list,
existing_facts: dict
) -> dict:
"""Extract facts from conversation and merge with existing ones"""
conv_text = "\n".join(
f"{t['role']}: {t['parts'][0]['text']}" for t in conversation
)
prompt = self.EXTRACTION_PROMPT.format(
existing_facts=json.dumps(existing_facts, ensure_ascii=False),
conversation=conv_text
)
response = self.client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
config={
"response_mime_type": "application/json"
}
)
try:
new_facts = json.loads(response.text)
# Merge with existing facts (new info overwrites)
merged = {**existing_facts, **new_facts}
return merged
except json.JSONDecodeError:
return existing_factsFact extraction doesn't need to run on every turn. Running it every 5–10 turns or at session end is more cost-efficient.
Multi-User and Multi-Session Management
Production environments need to handle thousands of concurrent users chatting simultaneously.
import uuid
from datetime import datetime
class ChatSessionManager:
"""Multi-user session management"""
def __init__(self, store: RedisSessionStore):
self.store = store
self.budget_manager = TokenBudgetManager()
self.fact_extractor = FactExtractor()
def create_session(self, user_id: str) -> str:
"""Create a new chat session"""
session_id = f"{user_id}:{uuid.uuid4().hex[:12]}"
memory = ConversationMemory(working_memory_limit=10)
# Carry over fact memory from previous sessions
user_facts = self._load_user_facts(user_id)
if user_facts:
memory.facts = user_facts
self.store.save_session(session_id, memory)
return session_id
def chat(self, session_id: str, user_message: str) -> str:
"""Process a chat message and return a response"""
# Restore session
memory = self.store.load_session(session_id)
if memory is None:
raise ValueError(f"Session not found: {session_id}")
# Add user message
memory.add_turn("user", user_message)
# Fit context within token budget
context = self.budget_manager.fit_to_budget(memory)
# Call Gemini API
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=context,
config={
"system_instruction": self._build_system_instruction(memory),
"temperature": 0.7,
"max_output_tokens": 4000
}
)
assistant_message = response.text
memory.add_turn("model", assistant_message)
# Extract facts periodically (every 10 turns)
if memory.total_turns % 10 == 0:
memory.facts = self.fact_extractor.extract(
memory.working_memory, memory.facts
)
# Save session
self.store.save_session(session_id, memory)
self.store.extend_ttl(session_id)
return assistant_message
def _build_system_instruction(self, memory: ConversationMemory) -> str:
"""Dynamically build the system instruction"""
base = "You are a helpful and knowledgeable assistant."
if memory.facts:
facts_str = ", ".join(f"{k}: {v}" for k, v in memory.facts.items())
base += f"\n\nKnown information about the user: {facts_str}"
if memory.summary:
base += f"\n\nConversation summary so far: {memory.summary}"
return base
def _load_user_facts(self, user_id: str) -> dict:
"""Load persistent fact memory for a user"""
raw = self.store.redis.get(f"user:facts:{user_id}")
if raw:
return json.loads(raw)
return {}
def save_user_facts(self, user_id: str, session_id: str):
"""Persist user facts when a session ends"""
memory = self.store.load_session(session_id)
if memory and memory.facts:
self.store.redis.set(
f"user:facts:{user_id}",
json.dumps(memory.facts, ensure_ascii=False)
)Fault Recovery and Graceful Degradation
Production systems must handle Redis connection failures, Gemini API rate limits, and network outages gracefully.
import logging
from functools import wraps
from tenacity import retry, stop_after_attempt, wait_exponential
logger = logging.getLogger(__name__)
class ResilientChatBot:
"""Fault-tolerant chatbot implementation"""
def __init__(self, session_manager: ChatSessionManager):
self.session_manager = session_manager
self.fallback_memory = {} # In-memory fallback store
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10)
)
def _call_gemini_with_retry(self, context: list, system_instruction: str) -> str:
"""Gemini API call with automatic retry"""
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=context,
config={
"system_instruction": system_instruction,
"temperature": 0.7,
"max_output_tokens": 4000
}
)
return response.text
def chat_with_fallback(self, session_id: str, user_message: str) -> str:
"""Chat with built-in fallback mechanisms"""
try:
return self.session_manager.chat(session_id, user_message)
except redis.ConnectionError:
logger.warning("Redis connection failed, using in-memory fallback")
return self._fallback_chat(session_id, user_message)
except Exception as e:
if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e):
logger.warning("Rate limited, falling back to smaller model")
return self._reduced_model_chat(session_id, user_message)
raise
def _fallback_chat(self, session_id: str, user_message: str) -> str:
"""In-memory fallback when Redis is unavailable"""
if session_id not in self.fallback_memory:
self.fallback_memory[session_id] = ConversationMemory()
memory = self.fallback_memory[session_id]
memory.add_turn("user", user_message)
context = memory.build_context()
response_text = self._call_gemini_with_retry(context, "You are a helpful assistant.")
memory.add_turn("model", response_text)
return response_text
def _reduced_model_chat(self, session_id: str, user_message: str) -> str:
"""Fall back to a smaller model when rate-limited"""
memory = self.session_manager.store.load_session(session_id)
if memory is None:
memory = ConversationMemory()
memory.add_turn("user", user_message)
# Use only the last 3 exchanges with Flash model
short_context = memory.working_memory[-6:]
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
response = client.models.generate_content(
model="gemini-2.5-flash", # Lighter model as fallback
contents=short_context,
config={"temperature": 0.7, "max_output_tokens": 2000}
)
response_text = response.text
memory.add_turn("model", response_text)
self.session_manager.store.save_session(session_id, memory)
return response_textIntegrating with the Context Caching API
Gemini API's context caching feature can significantly reduce costs for repeatedly sent system instructions and shared prefix content. Here's how it integrates with the long-term memory architecture.
from google import genai
from google.genai import types
class CachedContextManager:
"""Cost optimization through context caching"""
def __init__(self):
self.client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
self.cached_content = None
def create_cached_context(
self,
system_instruction: str,
shared_knowledge: str
):
"""Cache shared context for reuse"""
self.cached_content = self.client.caches.create(
model="gemini-2.5-pro",
config=types.CreateCachedContentConfig(
display_name="chatbot-shared-context",
system_instruction=system_instruction,
contents=[{
"role": "user",
"parts": [{"text": shared_knowledge}]
}, {
"role": "model",
"parts": [{"text": "Understood. I'll use this knowledge base to inform my responses."}]
}],
ttl="3600s" # 1-hour cache
)
)
return self.cached_content.name
def chat_with_cache(self, user_context: list) -> str:
"""Generate a response using cached context"""
if self.cached_content is None:
raise ValueError("Cache has not been created")
response = self.client.models.generate_content(
model="gemini-2.5-pro",
contents=user_context,
config=types.GenerateContentConfig(
cached_content=self.cached_content.name
)
)
return response.textFor a deeper dive into context caching strategies, see [Gemini API Context Caching for Cost Optimization]((/articles/gemini-api/gemini-api-context-caching-cost-optimization).
Production Monitoring and Metrics
Running a chatbot in production requires monitoring both conversation quality and system performance.
import time
from dataclasses import dataclass, field
@dataclass
class ChatMetrics:
"""Operational metrics for chatbot monitoring"""
total_sessions: int = 0
active_sessions: int = 0
total_messages: int = 0
total_input_tokens: int = 0
total_output_tokens: int = 0
avg_response_time_ms: float = 0.0
compression_events: int = 0 # Memory compression count
fallback_events: int = 0 # Fallback activation count
error_count: int = 0
_response_times: list = field(default_factory=list)
def record_response(self, duration_ms: float, input_tokens: int, output_tokens: int):
self.total_messages += 1
self.total_input_tokens += input_tokens
self.total_output_tokens += output_tokens
self._response_times.append(duration_ms)
# Rolling average of last 100 responses
recent = self._response_times[-100:]
self.avg_response_time_ms = sum(recent) / len(recent)
def to_dict(self) -> dict:
return {
"total_sessions": self.total_sessions,
"active_sessions": self.active_sessions,
"total_messages": self.total_messages,
"total_input_tokens": self.total_input_tokens,
"total_output_tokens": self.total_output_tokens,
"avg_response_time_ms": round(self.avg_response_time_ms, 2),
"compression_events": self.compression_events,
"fallback_events": self.fallback_events,
"error_rate": round(
self.error_count / max(self.total_messages, 1) * 100, 2
)
}Key metrics to monitor include average response time (target under 2 seconds), memory compression frequency (if too high, increase working memory limit), fallback activation rate (keep below 5%), and average turns per session as an engagement indicator.
Summary
Building long-term memory for production chatbots requires much more than simply appending conversation history. By combining a three-layer memory architecture (working memory, summary memory, and fact memory) with token budget control, session persistence, and fault recovery patterns, you can build cost-efficient, scalable chatbots that deliver excellent user experiences.
The key to a successful AI chat product lies in maximizing Gemini API's powerful context window and context caching features while designing for real-world infrastructure constraints.