Claude API Response Caching Strategy with Redis
If you're calling the Claude API in production, chances are a meaningful chunk of your requests are duplicates or near-duplicates: the same system prompt, the same few-shot examples, similar user questions asked minutes apart. A Redis caching layer sits in front of your Claude calls, returns identical responses for identical inputs instantly, and cuts both latency and token spend. This article walks through how to design that layer: what to hash as a cache key, how to pick TTLs, how to handle streaming, and where caching breaks down.
The core idea is simple: before calling the Claude API, compute a deterministic hash of everything that affects the output (model, system prompt, messages, temperature, tools), check Redis for that hash, and only call the API on a miss. The hard part is deciding what counts as "the same request" and how long a cached answer stays valid for your use case.
Why cache Claude responses at all
Three reasons dominate in practice:
- Latency. A Redis GET is sub-millisecond. A Claude completion, especially with a long context or extended thinking, can take seconds. For chatbots answering common questions (FAQs, onboarding flows, support macros), caching turns a multi-second wait into an instant response.
- Cost. Every cached hit is a request you don't pay for. If you have repeated system prompts across thousands of users (e.g., a fixed instruction set with only the user message varying slightly), a semantic or prefix cache can meaningfully reduce your bill.
- Rate limits. Fewer upstream calls means more headroom before you hit provider or gateway rate limits during traffic spikes.
Caching does not replace prompt caching features built into the model provider itself — Redis caching happens at the application layer, before the request ever reaches the API, and is orthogonal to any server-side prompt caching Claude might apply internally.
Designing the cache key
The cache key must capture everything that determines the output. A naive key on just the user's message will cause bugs the moment you change your system prompt or model version.
const crypto = require('crypto');
function buildCacheKey({ model, system, messages, temperature, tools }) {
const payload = JSON.stringify({ model, system, messages, temperature, tools });
const hash = crypto.createHash('sha256').update(payload).digest('hex');
return `claude:resp:${hash}`;
}
Notes on this:
- Include the model name. Switching from a fast model to a more capable one for the same prompt should never return a cached result meant for the other model.
- Include temperature and any sampling parameters. If temperature is above 0, caching identical inputs is arguably wrong anyway, since the whole point of temperature is variation. Most caching strategies only apply to temperature 0 or near-0, deterministic-style calls.
- Normalize whitespace and casing in the user message before hashing if you want near-duplicate questions to hit the same cache entry, but be aware this trades precision for hit rate.
- Version your key prefix (
claude:resp:v2:...) so you can invalidate everything at once when you change prompt structure.
Basic read-through cache with Redis
A read-through pattern is the simplest to reason about: check cache, call the API on miss, write the result back.
const redis = require('redis').createClient();
await redis.connect();
async function getClaudeResponse(request) {
const key = buildCacheKey(request);
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const response = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
}).then(r => r.json());
await redis.set(key, JSON.stringify(response), { EX: 3600 });
return response;
}
This works well for deterministic, non-streaming calls: classification tasks, structured extraction, fixed-answer FAQ bots. See /docs/messages for the request shape this hashes against.
Handling streaming responses
Streaming complicates caching because you're not caching one payload, you're caching a sequence of chunks. Two approaches work in practice:
- Cache the final assembled text, then on a cache hit, replay it as a synthetic stream (chunk it yourself and emit SSE-style events) so your frontend code doesn't need to know whether it was a live stream or a cached one.
- Only cache non-streaming calls and disable caching entirely for streamed conversational turns, since those are usually more varied anyway (real conversations rarely repeat verbatim). This is the simpler and more common choice.
If you're building on SubToAPI, streaming and non-streaming both go through the same /v1/messages endpoint (see /docs/streaming), so you can make this decision per-request based on whether the call is a good caching candidate — short, deterministic, high-repeat traffic — rather than an architectural constraint.
Choosing TTLs
TTL should reflect how quickly the "correct" answer for a given input changes:
- Static reference content (documentation Q&A, glossary lookups): hours to days.
- User-facing support answers: 15–60 minutes, since policies and product details do change.
- Anything referencing live data (stock prices, current time, account status) should either bypass caching or use a TTL under a minute — and honestly, if the prompt includes live data, the cache key changes every time anyway, so hit rate will be near zero regardless.
A practical middle ground is a short default TTL (5–15 minutes) with an explicit longer TTL only for endpoints you've manually reviewed as safe to cache longer.
Cache invalidation
Because the key is a hash of full request content, most invalidation problems solve themselves — change the system prompt, get a new key, old cached answers simply age out unused. The two invalidation triggers worth building explicit handling for:
- Prompt version bumps: increment the key prefix version so you don't need to scan and delete old keys.
- Manual flush for bad answers: if a cached response turns out to be wrong or stale, you need a way to delete a specific key by request hash rather than flushing the whole cache. Log the hash alongside the response so support tooling can look it up.
When not to cache
Skip caching for creative or conversational generation where variation is the point, for any call that includes user-specific PII you shouldn't be storing in Redis without encryption, and for tool-use requests where the tool's return value (not just the prompt) determines correctness — caching those risks serving stale tool results dressed up as fresh model output (see /docs/tools for how tool round-trips work).
Combine caching with retry and fallback logic upstream, and route your actual API calls through a service like SubToAPI so you get per-key usage metadata to verify your cache hit rate is actually reducing billed requests, not just adding latency without saving money. You can check this on the /pricing page relative to your current request volume, and get started at /signup with a free trial to test the setup on real traffic. The /docs/quickstart guide covers the basic request format this whole caching layer wraps around.
questions
Does caching Claude responses affect answer quality? Only if you cache non-deterministic calls. At temperature 0, cached answers are byte-identical to what the API would return, so quality is unaffected — the risk is caching stale or context-dependent answers too long.
Should I cache at the HTTP layer or the application layer? Application layer, using a hash of the full request payload as the key. HTTP-layer caching (e.g., a reverse proxy) doesn't understand which parts of a POST body matter for correctness and will either over-cache or under-cache.
How do I measure whether Redis caching is actually saving money? Track cache hit rate against your API usage dashboard over the same period. If you're using SubToAPI, per-key usage metadata makes it straightforward to compare requests made versus requests you'd expect without caching.