How to Cache Claude API Responses Locally
Caching Claude API responses locally means storing the model's output on disk or in memory on your own machine, keyed by the request content, so identical or near-identical calls return instantly without hitting the API again. This is different from a shared cache like Redis in production — local caching is mostly about development speed and cost control while you're building and testing, though the same techniques work in small single-server deployments too.
The short answer: hash your request payload (model, messages, system prompt, temperature) into a cache key, store the response as JSON in a local file, SQLite database, or an in-memory object, and check that store before making the API call. Below are the three most practical ways to do this, with working code for each.
Why cache locally at all
If you're iterating on a prompt, debugging a tool-use flow, or writing tests against Claude, you're often sending the exact same request dozens of times in an hour. Every one of those calls costs money and adds latency. Local caching solves both problems during development:
- Cost: you stop paying for identical requests you've already made
- Speed: cached responses return in milliseconds instead of seconds
- Reliability: your test suite doesn't fail because of a transient API error or rate limit
- Offline work: you can keep coding against previously seen prompts without network access
None of this is meant to replace real caching infrastructure in production — for that, a shared cache like Redis with proper invalidation is the right tool. Local caching is for your laptop and your CI pipeline.
Option 1: Simple file-based cache
The easiest approach is a flat JSON file per unique request, keyed by a hash of the payload.
import fs from "fs";
import path from "path";
import crypto from "crypto";
const CACHE_DIR = ".claude-cache";
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR);
function cacheKey(payload) {
const json = JSON.stringify(payload);
return crypto.createHash("sha256").update(json).digest("hex");
}
async function callWithCache(payload, apiCall) {
const key = cacheKey(payload);
const filePath = path.join(CACHE_DIR, `${key}.json`);
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
const response = await apiCall(payload);
fs.writeFileSync(filePath, JSON.stringify(response, null, 2));
return response;
}
This works with any Claude client — wrap your existing request function as apiCall and pass the message payload through callWithCache. The cache directory can be added to .gitignore or committed to your repo if you want deterministic fixtures for tests.
Option 2: SQLite for structured local caching
A flat-file cache gets messy once you want expiry, metadata, or to query which prompts are cached. SQLite is a good middle ground — no server to run, just a local .db file.
import Database from "better-sqlite3";
import crypto from "crypto";
const db = new Database("claude-cache.db");
db.exec(`
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
response TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
function cacheKey(payload) {
return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}
async function callWithCache(payload, apiCall, ttlMs = 24 * 60 * 60 * 1000) {
const key = cacheKey(payload);
const row = db.prepare("SELECT response, created_at FROM cache WHERE key = ?").get(key);
if (row && Date.now() - row.created_at < ttlMs) {
return JSON.parse(row.response);
}
const response = await apiCall(payload);
db.prepare(
"INSERT OR REPLACE INTO cache (key, response, created_at) VALUES (?, ?, ?)"
).run(key, JSON.stringify(response), Date.now());
return response;
}
Adding ttlMs gives you expiry, so long-running dev sessions don't serve stale responses forever. This is a good default if you're building an internal tool or a test harness that runs regularly.
Option 3: In-memory cache for a single process
If you just need caching within one script run or one test file, skip the disk entirely:
const memoryCache = new Map();
async function callWithCache(payload, apiCall) {
const key = JSON.stringify(payload);
if (memoryCache.has(key)) return memoryCache.get(key);
const response = await apiCall(payload);
memoryCache.set(key, response);
return response;
}
This resets every time the process restarts, which is exactly what you want for short-lived scripts where you don't need persistence across runs.
Things to watch out for
Non-determinism: Claude's output can vary between identical requests unless temperature is set to 0 and the prompt is fully deterministic. If your cache key doesn't include temperature and other sampling parameters, you'll get cache hits that don't match what a live call would return today. Always include every parameter that affects output in your hash, not just the messages.
Streaming responses: if you're using streaming, cache the fully assembled response after the stream completes, then replay it as a single object (or a fake stream) on cache hits. Don't try to cache partial chunks — reconstruct the full message first.
Cache staleness for tool use: if a request includes tool results that depend on live data (current time, external API responses), caching the full request/response pair can silently serve outdated tool outputs. Exclude tool-result-bearing requests from your cache, or key them narrowly enough that stale data can't leak in.
System prompt changes: if you tweak your system prompt and don't include it in the hash, you'll get cache hits against the old prompt's output. Hash the entire payload, not just the user message.
Where this fits with SubToAPI
If you're using SubToAPI to turn your Claude access into an HTTPS API, local caching sits entirely in your application layer — SubToAPI issues sub_live_... keys and handles streaming, tool use and usage metadata, but request caching is something you build on top, exactly as shown above, whether you're calling the Anthropic API directly or going through SubToAPI's endpoint. Check the docs for request and response formats, and see streaming and tools for details on the fields you'll want to include in your cache key. If you're just getting started, the quickstart walks through your first authenticated request, and you can grab a key at signup.
FAQ
Does caching work if I use streaming responses? Yes, but cache the assembled final message after the stream finishes rather than individual chunks, then serve it as a complete object (or replay it as a fake stream) on subsequent identical requests.
Will a local cache reduce my Claude API costs in production? It can for repeated identical requests, but production traffic is rarely identical request-for-request. Local caching is most valuable during development and testing; for production, a shared cache with proper invalidation logic works better.
What should I include in the cache key besides the prompt? Include the model name, full message history, system prompt, temperature, max tokens, and any tool definitions — anything that affects the output. Leaving one of these out causes stale or incorrect cache hits.