Redis for Fast Short-Term Agent State
Use Redis as a shared, low-latency store for conversation history and scratchpad fields so any worker can continue a session after the previous request finished.
Summary
Serialize a session document (messages + scratchpad + optional tool cache metadata) under a tenant-scoped key, set a TTL, and load-modify-save on each turn with optimistic concurrency or a lock when concurrent writers exist.
Recipe
- Run Redis locally or use a managed instance; enable AUTH and TLS in non-dev environments.
- Choose a key layout:
agent:stm:{tenant_id}:{user_id}:{session_id}(adjust separators to your house style). - Define a JSON schema for the session payload (version field included).
- On each turn: GET → deserialize → append messages / update scratchpad → enforce size limits → SET with TTL refresh.
- Use short TTLs for abandoned chats (hours to days) and explicit delete on user "clear chat."
- Add metrics: hit rate, payload bytes, trim events, lock contention.
Working Example
from __future__ import annotations
import json
import os
import time
from typing import Any
import redis # pip install redis
r = redis.Redis.from_url(os.environ.get("REDIS_URL", "redis://localhost:6379/0"), decode_responses=True)
SESSION_TTL_SEC = 60 * 60 * 24 # 24h; tune per product
def session_key(tenant_id: str, user_id: str, session_id: str) -> str:
return f"agent:stm:{tenant_id}:{user_id}:{session_id}"
def new_session() -> dict[str, Any]:
return {
"v": 1,
"messages": [
{"role": "system", "content": "You are a brief, careful assistant."},
],
"scratchpad": {},
"updated_at": time.time(),
}
def load_session(tenant_id: str, user_id: str, session_id: str) -> dict[str, Any]:
raw = r.get(session_key(tenant_id, user_id, session_id))
if not raw:
return new_session()
return json.loads(raw)
def save_session(tenant_id: str, user_id: str, session_id: str, session: dict[str, Any]) -> None:
session["updated_at"] = time.time()
key = session_key(tenant_id, user_id, session_id)
r.set(key, json.dumps(session), ex=SESSION_TTL_SEC)
def append_turn(tenant_id: str, user_id: str, session_id: str, user_text: str, assistant_text: str) -> dict:
sess = load_session(tenant_id, user_id, session_id)
sess["messages"].append({"role": "user", "content": user_text})
sess["messages"].append({"role": "assistant", "content": assistant_text})
# Optional: call trim_to_budget(sess["messages"]) before save
save_session(tenant_id, user_id, session_id, sess)
return sessFor concurrent turns on the same session, wrap the read-modify-write in a Redis lock (SET key NX EX) or store a version and use WATCH/MULTI so two writers do not clobber each other.
Deep Dive
Why Redis fits short-term memory
Agent turns are chatty: every user message may hit a different app instance.
In-process dicts break under horizontal scale and deploys.
Redis gives sub-millisecond reads for multi-kilobyte session blobs and first-class TTL so memory reclaims itself when users leave.
What to store vs not store
| Store in Redis session | Prefer elsewhere |
|---|---|
| Recent messages (bounded) | Multi-megabyte tool payloads (object storage + pointer) |
| Scratchpad JSON | Long-term user profile (primary DB) |
| Tool cache keys + small values | Secrets (vault / KMS) |
| Session version / lock fields | Full observability traces (log pipeline) |
Key design and isolation
Keys must include tenant and user (or an equivalent authz scope), not only a client-supplied session uuid.
If the client can guess session ids, missing scope checks become cross-tenant reads.
Authorize that the authenticated principal owns (tenant, user, session) before GET/SET.
TTL and refresh
Refresh TTL on activity so live chats do not expire mid-conversation.
Do not use infinite TTL for chat state unless you have a separate purge job.
Compliance may require shorter retention than product convenience wants - document the choice.
Size limits
Cap serialized session size (for example 256 KB-1 MB) and trim history before save.
Oversized values hurt Redis memory and request latency.
If you need more history, keep cold transcript in Postgres/S3 and only hot-tail in Redis.
Consistency patterns
| Pattern | Use when |
|---|---|
| Last-write-wins SET | Low concurrency, simple bots |
| Version field + check | Detect lost updates; retry |
| Distributed lock per session | Multi-step tool runs that must not interleave |
| Streams / lists for messages | Append-only logs with separate scratchpad hash |
LangGraph and similar runtimes may offer Redis checkpointers; still apply the same keying, TTL, and size rules underneath.
Gotchas
- Session id only keys.
session:{uuid}without tenant scope is a multi-tenant footgun. - No TTL. Redis fills with zombie chats until OOM policies surprise you.
- Storing API keys in session JSON. Never; inject secrets at runtime from env/secret managers.
- Unbounded message append. Always trim or summarize before SET.
- Race on parallel tabs. Two browser tabs can interleave turns; lock or serialize per session.
decode_responsesand binary. Be consistent; tool payloads with bytes need base64 or external storage.- Hot keys. One celebrity session is fine; millions of large sessions need memory monitoring and eviction policy awareness.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Redis JSON blob (this recipe) | Simple, fast, TTL-native | Large values need discipline |
| Redis Hash fields | Partial updates to scratchpad | Awkward for message arrays |
| Postgres session row | Strong durability, SQL audit | Higher latency unless pooled carefully |
| Framework checkpoint (LangGraph etc.) | Integrated with graph state | Still need ops, TTL, isolation |
| Sticky in-memory + sticky LB | Zero external dep | Fragile deploys and scale-out |
FAQs
Is Redis durable enough for chat history?
For short-term session continuity, yes if you accept possible loss on failure modes you configure (AOF/RDB settings). For legal-grade transcripts, also write to a durable store.
Should every tool result go into Redis?
Only compact results or pointers. Large HTML and PDFs belong in object storage with a reference in the session.
How do I clear a user's memory?
DELETE the session key (and any related cache keys). Expose this as a product action when regulations or UX require it.
Can I use Redis Cluster?
Yes. Keep keys that must be multi-key transactional on the same hash tag if you use multi-key ops; prefer single-key session documents to stay simple.
What about serverless Redis / Upstash-style HTTP?
Fine for many agents; watch request overhead and payload size limits. The key layout and TTL rules do not change.
How does this relate to long-term memory?
Redis session state dies with TTL. Long-term facts should be extracted into a DB or vector store with their own lifecycle.
Do I need Redis if I have only one server process?
Not required for a prototype. Introduce Redis when you add a second worker, rolling deploys, or multi-instance hosting.
What eviction policy should I set?
Prefer not relying on allkeys-lru for correctness; use TTLs. If you must, understand that LRU can drop live sessions under memory pressure.
Related
Related: Short-Term Memory Basics
Related: Session Isolation
Related: Scratchpad Patterns
Related: Managing Conversation History Within a Context Budget
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.