How to Cache Claude API Responses (Full Guide)
Caching Claude API responses means storing the output of a completed request so that an identical or near-identical future request can be served from storage instead of triggering a new model call. This is one of the highest-leverage things you can do if you're running Claude in production: it cuts latency from seconds to milliseconds on cache hits and reduces token spend on repeated or predictable queries.
The short version: pick a cache key that captures everything that affects the output (model, system prompt, messages, temperature, tools), store the response in something fast like Redis or an in-memory LRU, set a sensible TTL based on how often your underlying data changes, and add a bypass mechanism for requests that must always be fresh. The rest of this article walks through each piece.
When caching actually helps
Caching Claude responses is worth doing when:
- The same prompt (or a normalized version of it) recurs. FAQ bots, documentation assistants, classification pipelines, and support triage tools often see the same or very similar inputs repeatedly.
- You're doing deterministic tasks. Classification, extraction, summarization of static documents, and structured data generation are good candidates because the "correct" answer doesn't change between calls.
- Latency matters more than freshness. If users are waiting on a response and the underlying content hasn't changed, serving a cached answer in 20ms beats waiting 2-3 seconds for a fresh generation.
Caching is a poor fit for open-ended conversational turns, anything involving current events, or requests where temperature is intentionally high because you want varied output each time.
Building a cache key
The cache key has to represent everything that determines the response. If you leave something out, you'll serve stale or wrong answers; if you include too much (like a timestamp), you'll never get a hit.
At minimum, hash together:
- the model name
- the full system prompt
- the messages array (role + content, in order)
- temperature, top_p, max_tokens
- any tool definitions passed in the request
import crypto from "crypto";
function buildCacheKey(request) {
const normalized = JSON.stringify({
model: request.model,
system: request.system,
messages: request.messages,
temperature: request.temperature ?? 1,
max_tokens: request.max_tokens,
tools: request.tools ?? null,
});
return crypto.createHash("sha256").update(normalized).digest("hex");
}
For user-facing chat, don't cache multi-turn conversations wholesale — cache individual deterministic sub-tasks instead (e.g., "classify this ticket" or "summarize this document"), where the input is stable and small.
Where to store cached responses
Three common options, in order of complexity:
In-memory LRU cache — fastest, simplest, but scoped to a single process. Fine for scripts, cron jobs, and low-traffic services. Use something like lru-cache in Node or functools.lru_cache in Python for straightforward cases.
Redis — the standard choice for anything running more than one instance. Supports TTLs natively, is fast enough not to add noticeable overhead, and is easy to invalidate selectively by key or pattern.
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
async function getCachedOrGenerate(key, generateFn, ttlSeconds = 3600) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const result = await generateFn();
await redis.set(key, JSON.stringify(result), { EX: ttlSeconds });
return result;
}
A database table with a hash column — useful when you need to audit cached responses, join them with other business data, or keep them longer than Redis TTLs are comfortable with. Slower than Redis but durable and queryable.
Setting TTLs and invalidation rules
TTL should reflect how often the ground truth behind your prompt changes, not an arbitrary number:
- Static reference material (docs, policies, product descriptions): hours to days
- User-generated content that updates occasionally: minutes to an hour
- Anything tied to live data (prices, inventory, current status): don't cache, or cache for seconds only
Build an explicit invalidation path rather than relying purely on TTL expiry. If the source document changes, delete the corresponding cache key immediately — waiting for a TTL to lapse means serving wrong answers in the meantime. Tagging cache keys by source ID (e.g., doc:1234:summary) makes targeted invalidation easy with Redis's DEL or pattern-based SCAN.
Caching at the API layer vs. the application layer
You can implement caching yourself around any Claude API call, as written above. If you're already routing Claude through a gateway for API key management, usage tracking, or team billing, it's worth checking whether that layer can also help with caching-adjacent concerns — for example, exposing per-key usage metadata so you can see which cached vs. uncached requests are costing the most.
SubToAPI turns your Claude access into a standard HTTPS API with per-application keys and usage metadata on every response, which makes it straightforward to instrument your own cache hit/miss tracking on top. See the quickstart or the messages docs for request/response shapes if you're wiring caching into an existing integration.
A minimal caching wrapper
Putting it together as a reusable function:
async function cachedClaudeCall(request, { ttl = 3600 } = {}) {
const key = `claude:${buildCacheKey(request)}`;
const cached = await redis.get(key);
if (cached) {
return { ...JSON.parse(cached), cached: true };
}
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: ttl });
return { ...response, cached: false };
}
Log the cached flag on every call so you can measure hit rate and confirm the caching layer is actually saving money over time.
Streaming responses and caching
Streamed responses (streaming docs) can still be cached — collect the full stream server-side, store the assembled result, and replay it as a single response (or a synthetic fast stream) on cache hits. Don't try to cache partial chunks independently; cache the completed output only.
questions
Does caching change the model's answer over time? Yes — a cached response is frozen at generation time. If the model would give a different or better answer today, you won't see it until the cache entry expires or is invalidated. This is fine for stable facts, risky for anything time-sensitive.
Should I cache tool-use responses? Only cache the final response after tool execution completes, and include the tool definitions in your cache key (tool use docs). Don't cache intermediate tool-call requests, since those depend on live external state.
What's a reasonable cache hit rate to expect? It depends heavily on traffic patterns. FAQ-style or classification workloads with repeated inputs often see 30-60% hit rates; open-ended chat typically sees much lower rates unless you're caching sub-tasks rather than whole conversations.