Claude API Agent Memory with Redis: A Practical Guide
If you're building a Claude API agent that needs to remember things between requests — a user's preferences, prior tool calls, or the last ten turns of a conversation — Redis is the standard answer. The Claude API itself is stateless: every request needs the full conversation history in the messages array, so "memory" is really just your application correctly storing and replaying state. Redis is a good fit because it's fast, supports TTLs for automatic expiry, and has native data structures (lists, hashes, sorted sets) that map cleanly onto conversation turns, session metadata, and semantic caches.
This guide covers the three memory patterns most agents actually need: short-term conversation buffers, long-term key-value facts, and semantic/vector recall — all backed by Redis, with code you can adapt directly.
Why Redis for agent memory
Claude's context window is large, but sending the entire history on every call is slow and expensive. You want to:
- Store conversation turns durably but retrieve only what's relevant
- Expire stale sessions automatically (TTL)
- Support multiple concurrent users/agents without database contention
- Keep latency low — memory lookups shouldn't be the bottleneck in your request path
Redis handles all of this natively. A Postgres table works too, but you'll end up reimplementing TTLs and hot-path caching that Redis gives you for free.
Pattern 1: Short-term conversation buffer
The simplest and most common case: store the last N turns per session as a Redis list, and replay them into the messages array on each request.
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const MAX_TURNS = 20;
const TTL_SECONDS = 60 * 60 * 24; // 24h
async function appendTurn(sessionId, role, content) {
const key = `session:${sessionId}:messages`;
await redis.rPush(key, JSON.stringify({ role, content }));
await redis.lTrim(key, -MAX_TURNS, -1);
await redis.expire(key, TTL_SECONDS);
}
async function getHistory(sessionId) {
const key = `session:${sessionId}:messages`;
const raw = await redis.lRange(key, 0, -1);
return raw.map((r) => JSON.parse(r));
}
On each new user message, append it, fetch the trimmed history, and send it to Claude:
async function chat(sessionId, userMessage) {
await appendTurn(sessionId, "user", userMessage);
const messages = await getHistory(sessionId);
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages,
}),
});
const data = await res.json();
const reply = data.content[0].text;
await appendTurn(sessionId, "assistant", reply);
return reply;
}
This is enough for most chatbots and support agents. The lTrim keeps token usage bounded without manual pruning logic, and the TTL means abandoned sessions clean themselves up.
Pattern 2: Long-term facts with Redis hashes
Conversation buffers lose information once trimmed. For facts that should persist beyond a session — a user's name, timezone, subscription tier, or preferences extracted mid-conversation — use a Redis hash keyed by user ID, not session ID.
async function rememberFact(userId, field, value) {
await redis.hSet(`user:${userId}:facts`, field, value);
}
async function recallFacts(userId) {
return await redis.hGetAll(`user:${userId}:facts`);
}
Inject these facts into the system prompt rather than the message history, since they're static context rather than dialogue:
const facts = await recallFacts(userId);
const systemPrompt = `You are a helpful assistant. Known facts about this user: ${JSON.stringify(facts)}`;
A good practice is to have Claude itself flag when something worth remembering comes up — using tool use with a save_fact tool the agent can call, which your backend then writes to Redis. This avoids regex-based extraction and lets the model decide what's actually durable.
Pattern 3: Semantic recall for long histories
Once conversations grow beyond what fits comfortably in a buffer, a pure list-based approach starts truncating useful context. The fix is a semantic memory layer: embed each turn, store the vector in Redis (via RedisSearch / Redis Stack's vector similarity features), and retrieve only the top-k relevant turns before calling Claude.
The flow:
- After each turn, generate an embedding (OpenAI, Voyage, or any embedding model) and store it with
FT.CREATE/HSETin a Redis vector index. - On a new user message, embed it and run a KNN search against stored turns.
- Pull the top 3–5 matches plus the last few raw turns, and build your
messagesarray from that combined set.
// Pseudocode — actual syntax depends on your Redis client
const results = await redis.ft.search("idx:memory",
`*=>[KNN 5 @embedding $vec]`,
{ PARAMS: { vec: queryEmbedding }, DIALECT: 2 }
);
This keeps token usage flat even for agents that run for thousands of turns, because you're no longer sending the full history — just the relevant slices.
Putting it together in a production agent
A typical architecture:
- Session memory (Redis list, short TTL) — recent turns
- User memory (Redis hash, long TTL or none) — durable facts
- Semantic memory (Redis vector index) — optional, for long-running agents
- Claude API calls — routed through SubToAPI, which gives you a stable
sub_live_...key, streaming support, and usage metadata per request so you can monitor how much context each memory strategy is actually consuming
If you're prototyping, start with Pattern 1 — it solves 80% of memory needs with about 20 lines of code. Add the hash-based long-term store once you need personalization across sessions, and only reach for vector search once conversations genuinely outgrow a trimmed buffer. Check the quickstart and messages docs if you're wiring this into SubToAPI's endpoint for the first time.
FAQ
Does Claude have built-in memory I can use instead of Redis? No. The Claude API is stateless — each request is independent, and the model has no memory of previous calls unless you include that context in the messages array yourself. Redis (or another store) is required to implement memory.
How much conversation history should I store per session? Enough to cover the model's useful context without bloating token usage — typically the last 10–30 turns for a chat agent. Use lTrim or similar to cap list length, and rely on summarization or semantic retrieval once conversations grow longer than that.
Is Redis better than a SQL database for this use case? For hot-path, short-lived session data, yes — Redis's in-memory speed and native TTL support fit the access pattern better. For durable, queryable long-term records (billing history, audit logs), a SQL database is still the right choice; many agents use both together.