
Hey guys, Mr. Technology here.
The fastest way to ship a chat feature that remembers what the user said ten minutes ago is Redis. Not Postgres. Not Mongo. Not a vector DB. A few hundred lines of Python, an HSET, and a sane eviction policy. Here is the build.
Your chat endpoint has no memory between turns. Or it has memory but the session store OOMs at ten thousand users. Or you are storing full conversation histories in Postgres and your SELECT count(*) is climbing on every query. Redis with a per-session key, a sliding window of recent messages, and a 24-hour TTL is the boring, correct answer.
Postgres works fine for hundreds of users and breaks at thousands. Vector DBs are overkill — you do not need semantic retrieval to remember the last twenty messages. Dedicated conversation stores (LangChain memory backends, Zep, etc.) lock you in to a specific framework. Redis is fast, ubiquitous, has hash storage that maps cleanly to message lists, and you already have it for caching. One dependency, one mental model, one thing to debug at 2am.
One Redis key per session. Use a hash: llm:session:{session_id} with fields user_id, created_at, last_active, and a JSON-serialized message list.
import redis
import json
from time import time
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def session_key(sid: str) -> str:
return f"llm:session:{sid}"
def create_session(sid: str, user_id: str) -> None:
r.hset(session_key(sid), mapping={
"user_id": user_id,
"created_at": int(time()),
"last_active": int(time()),
"messages": json.dumps([]),
})
r.expire(session_key(sid), 86400) # 24h TTL on creationRun it locally with docker run -p 6379:6379 redis:7-alpine and you are coding in under a minute.
You want append on write and full load on read. Keep it boring.
def append_message(sid: str, role: str, content: str, max_messages: int = 20) -> None:
key = session_key(sid)
raw = r.hget(key, "messages") or "[]"
msgs = json.loads(raw)
msgs.append({"role": role, "content": content, "ts": int(time())})
msgs = msgs[-max_messages:] # sliding window
r.hset(key, mapping={
"messages": json.dumps(msgs),
"last_active": int(time()),
})
r.expire(key, 86400) # refresh TTL on activity
def load_messages(sid: str) -> list[dict]:
raw = r.hget(session_key(sid), "messages")
if not raw:
return []
return json.loads(raw)The max_messages cap is your context-window insurance. For Claude Sonnet at 200k tokens you can go higher; for gpt-4o-mini at 128k, twenty messages covers most product flows. Set it on the model, not the store.
from openai import OpenAI
client = OpenAI()
def chat(sid: str, user_message: str) -> str:
history = load_messages(sid)
history.append({"role": "user", "content": user_message})
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=history,
)
reply = resp.choices[0].message.content
append_message(sid, "user", user_message)
append_message(sid, "assistant", reply)
return replyThat is a complete session layer. About forty lines of code and one Redis call per turn.
expire(key, 86400) on every activity means long sessions renew forever. An abandoned tab from a week ago is still alive in Redis, eating RAM. Set a hard max-age checked on read (created_at + 7d is a common cap), or back the cache with ZSET-based LRU eviction. Otherwise your INFO memory will tell you a sad story at the end of the month when the bill arrives.
Function-calling agents return raw tool payloads — five-megabyte JSON blobs, base64 images, the entire contents of whatever file the agent just read. They will eat your context window and your HSET bandwidth on the next turn. Strip them to a one-line summary before persisting:
def tool_summary(tool_call_result: dict) -> str:
return f"[{tool_call_result['name']}] {str(tool_call_result['output'])[:500]}"Persist only what the next turn actually needs. This is the lesson every team learns after their fourth OpenAI bill shock.
Skip Redis when you need semantic recall — "the user mentioned their cat three conversations ago" is a vector DB job. Skip it when you have strict compliance requirements and need every message audit-logged with WORM semantics — that is Postgres plus an append-only ledger. Skip it when your sessions are measured in months, not days — Redis lives in RAM and RAM is money past a certain scale.
For 90% of chat products shipping this year, Redis is the boring right answer. Build it once, forget about it, and ship the feature.
— Mr. Technology
*Packages: redis-py 5.x. Hash storage key: llm:session:{session_id}. decode_responses=True returns str instead of bytes and saves a thousand .decode() calls. Sliding window default: 20 messages (covers about 5 turns of typical product chat). For multi-tenant, prefix the key with a tenant id: llm:session:{tenant}:{session_id}. Production tip: set maxmemory-policy allkeys-lru in redis.conf so unused sessions evict gracefully if you ever blow past the budget.*