Stop Treating Prompts as Strings: Engineering Prompt Caching in AI Applications
The author argues that prompt caching shouldn't be treated as caching the entire request result. Instead, prompts should be split into a base policy layer, a business knowledge layer, and a request context layer, caching only the stable segments.
The author turns prompt caching from a request-result cache into layered, versionable, observable infrastructure, and lays out concrete approaches for cache key protocols and invalidation control surfaces.
When many teams build AI applications, their first version often crams the entire prompt into one long string. System role, business rules, tool descriptions, JSON schema, few-shot examples, user input—all concatenated and sent straight to the model. That works fine for a demo, but once real traffic hits, problems surface fast.
The most obvious one is cost. Every request resends a large block of stable context—even if the user only changed one sentence, the system still has to process the full prompt again. Then there's latency. The longer the context, the slower the first token, and the longer the tool-call chain, the more sluggish the product feels. More insidious are governance issues. If the prompt changes by one line, which requests does it affect? Is the cache still valid? After a tenant's permission rules are updated, will stale cache entries keep getting hits? Without proper engineering design, all of these eventually turn into production incidents.
This article is about a very concrete practice: don't treat Prompt Cache as a simple "request result cache," but as a layer of infrastructure for your AI application. What it caches isn't the model's final answer, but the reusable, versionable, observable stable fragments within the prompt.
I. Why Hashing the Whole Prompt Usually Isn't Enough
The most obvious approach is to hash the full prompt and store the model's response in Redis. That works for FAQs, fixed Q&A, and offline summarization, but it quickly breaks down in interactive AI applications.
The reason is simple: real requests contain lots of small variations:
- User input differs every time
- Current time, region, and language preference change
- Retrieval results change
- Tool state changes
- Tenant permissions and business configuration change
- Output format may be adjusted per scenario
If any one of these changes, the whole-prompt hash is completely different. Hit rate is low, and the cache is basically useless. Worse, to boost hit rate, teams may strip fields from the hash that shouldn't be ignored, leading to permission cross-talk or dirty data.
So the key to Prompt Cache isn't "caching the whole prompt"—it's identifying the stable layers and the variable layers within the prompt.
II. Split the Prompt into Three Layers
I'd rather split the prompt in an AI application into three layers.
The first layer is the base policy layer. It covers system identity, response boundaries, security policy, output language, and format constraints. This layer is usually tied to the application version and changes least often.
The second layer is the business knowledge layer. It covers product descriptions, tool lists, field schemas, few-shot examples, and common error-handling patterns. This layer changes as the business iterates, but not on every request.
The third layer is the request context layer. It covers the user's current input, retrieved snippets, conversation summaries, temporary variables, and tool return values. This layer changes fastest and usually isn't suitable for long-term caching.
The engineering goal is to keep the first and second layers as stable and cacheable as possible, while the third layer only participates in the final call and doesn't pollute the stable cache.
Think of it like chunks in a frontend build. System rules are like the vendor chunk, business schemas are like feature chunks, and user input is like runtime data. You wouldn't rebuild the entire frontend just because a user clicked a button, and likewise you shouldn't make the model reprocess all the stable context just because the user changed one sentence.
III. Cache Keys Aren't String Concatenation—They're a Version Protocol
The place where Prompt Cache most easily goes wrong is the cache key. A safe key should include at least these dimensions:
- appId: which application
- tenantId: which tenant
- promptVersion: prompt template version
- policyVersion: permission and security policy version
- schemaVersion: output structure version
- locale: language and region
- modelFamily: model family or capability tier
- segmentHash: hash of the stable fragment content
Note that userId isn't in every key. Whether userId is needed depends on whether the cached fragment contains user-private information. System rules and generic schemas shouldn't be split per user, or hit rate will be terrible. Fragments containing user permissions, user profiles, or private document summaries must be isolated by user or permission boundary.
A common mistake is using only the prompt text hash. It looks clean, but it lacks version semantics. For example, if the permission policy isn't written into the prompt text but is enforced at the server-side tool layer, then when the permission policy is upgraded, old cache entries may still get hits. This kind of bug is hard to track down, because from the prompt text everything looks unchanged.
IV. A Minimal Implementation
Here's a simplified TypeScript-style implementation. It's not a complete framework, but it's enough to express the core design.
import crypto from "node:crypto";
type Segment = {
name: string;
content: string;
scope: "global" | "tenant" | "user";
version: string;
};
type CacheContext = {
appId: string;
tenantId: string;
userId?: string;
modelFamily: string;
policyVersion: string;
locale: string;
};
function sha256(input: string) {
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
}
function segmentKey(ctx: CacheContext, seg: Segment) {
const parts = [
"prompt-segment",
ctx.appId,
ctx.modelFamily,
ctx.locale,
`policy:${ctx.policyVersion}`,
`segment:${seg.name}`,
`v:${seg.version}`,
`h:${sha256(seg.content)}`,
];
if (seg.scope === "tenant") parts.push(`tenant:${ctx.tenantId}`);
if (seg.scope === "user") parts.push(`tenant:${ctx.tenantId}`, `user:${ctx.userId ?? "anonymous"}`);
return parts.join(":");
}
This code has two key points. First, content hash and explicit version coexist. The hash ensures that content changes invalidate the cache, and the version ensures that semantic changes can be proactively invalidated. Second, scope explicitly expresses the isolation boundary, avoiding both the crude approach of splitting every cache per user and letting user-private information into the global cache.
In a real system, the cached value can be a preprocessed prompt fragment, token counts, a compressed summary, a compiled tool schema, or a prefix cache reference supported by certain model providers. Different models have different capabilities, but the higher-level ideas of segmentation, versioning, and observability are universal.
V. Don't Just Look at Hit Rate
Many teams only look at hit rate after shipping a cache, and that's not enough. Prompt Cache should track at least five kinds of metrics.
First is segment hit rate. A low hit rate for whole requests doesn't mean the cache is worthless; a high hit rate for stable fragments is what matters.
Second is saved tokens. How many input tokens did cache hits save? That's closer to the cost benefit than the number of hits.
Third is first token latency. What users feel is the first token time, not the server-side function execution time.
Fourth is stale prevention. The number of proactive invalidations caused by version, permission, or schema changes needs to be recorded.
Fifth is fallback rate. When the cache system fails, how many requests fall back to the no-cache path? If fallback is too high, the cache layer is unstable; if there's no fallback, the cache layer may have become a single point of failure.
You can record each call with the structure below.
type PromptCacheTrace = {
requestId: string;
segments: Array<{
name: string;
scope: string;
hit: boolean;
inputTokens: number;
savedTokens: number;
version: string;
}>;
latencyMs: number;
firstTokenMs?: number;
fallback: boolean;
invalidationReason?: string;
};
These traces don't need to store the full text permanently. In fact, production environments should avoid writing users' raw prompts into logs. Recording the segment name, version, hash, token count, and hit status is usually enough.
VI. A Reproducible Experiment
To avoid just talking concepts, I wrote a small experiment. It doesn't call a real model; it simulates the cost and latency of three strategies:
- no-cache: process the full prompt every time
- full-hash-cache: only hit when the full prompt is identical
- segment-cache: stable fragments can be hit individually
const requests = Array.from({ length: 1000 }, (_, i) => ({
tenant: `t${i % 10}`,
schemaVersion: i < 700 ? "v1" : "v2",
userInput: `question-${i}`,
retrievedDoc: `doc-${i % 80}`,
}));
const baseTokens = 1800;
const schemaTokens = 900;
const dynamicTokens = 500;
function simulate(strategy) {
const cache = new Set();
let costTokens = 0;
let hits = 0;
for (const r of requests) {
if (strategy === "no-cache") {
costTokens += baseTokens + schemaTokens + dynamicTokens;
continue;
}
if (strategy === "full-hash-cache") {
const key = `${r.tenant}:${r.schemaVersion}:${r.userInput}:${r.retrievedDoc}`;
if (cache.has(key)) hits++;
else {
cache.add(key);
costTokens += baseTokens + schemaTokens + dynamicTokens;
}
continue;
}
if (strategy === "segment-cache") {
const baseKey = `base:${r.tenant}`;
const schemaKey = `schema:${r.schemaVersion}`;
if (cache.has(baseKey)) hits++; else { cache.add(baseKey); costTokens += baseTokens; }
if (cache.has(schemaKey)) hits++; else { cache.add(schemaKey); costTokens += schemaTokens; }
costTokens += dynamicTokens;
}
}
return { strategy, hits, costTokens };
}
console.table([
simulate("no-cache"),
simulate("full-hash-cache"),
simulate("segment-cache"),
]);
In this simulation, full-hash-cache has almost no benefit, because user input differs every time. segment-cache shows clear benefit, because tenant base rules and schema versions are reused heavily. Real online results will depend on the model, request structure, and business distribution, but the direction is usually the same: caching stable fragments is more reliable than betting on whole-request repetition.
One thing to emphasize here: this isn't a vendor pricing benchmark, nor a model performance evaluation. It's just a way to help teams verify whether their prompt structure has reuse potential. Before actually shipping, you should record tokens, latency, and invalidation reasons in shadow traffic, then decide which fragments are worth caching.
VII. Cache Invalidation Has to Be Operable
Prompt Cache isn't done once you've written the key. Online, the invalidation strategy matters more.
I recommend keeping at least three invalidation methods.
First, version invalidation. When releasing a new prompt, new schema, or new permission rule, just bump the version number. This is the most controllable approach.
Second, scope invalidation. When a tenant's configuration changes, only clear the cache in that tenant scope, without affecting the global cache.
Third, emergency circuit-breaking. When cache contamination or abnormal output is detected, you can quickly turn off caching by app, tenant, segment, or modelFamily.
Don't rely only on TTL. TTL can serve as a fallback, but it can't express business semantics. Permission changes need to take effect immediately—you can't wait for the cache to expire naturally. Schema releases also shouldn't rely on "waiting ten minutes."
A practical control plane might look like this:
{
"appId": "support-agent",
"cacheEnabled": true,
"disabledSegments": ["user-private-profile"],
"minPromptVersion": "2026-10-12.3",
"tenantOverrides": {
"tenant-a": {
"cacheEnabled": false,
"reason": "permission policy migration"
}
}
}
This kind of configuration is best read by the server side, not hardcoded in application code. AI applications change fast, and if every cache shutdown requires a new release, incident response becomes very passive.
VIII. Common Pitfalls
The first pitfall is caching retrieval results long-term. In RAG scenarios, retrieval results are often tied to index version, permissions, and time. You can cache the normalized result of a retrieval query, and you can cache public knowledge fragments, but don't ignore document permissions and index version.
The second pitfall is mixing tool return values into the stable layer. Tool results usually represent current state—orders, inventory, approval flows, account balances. They should be request context, not global prompt assets.
The third pitfall is caching the wrong format. If the model fails to output JSON and you cache the failed result, subsequent requests will keep hitting the same pit. When caching final answers, you must only cache results that pass schema validation.
The fourth pitfall is not distinguishing "saving input tokens" from "saving end-to-end time." If the bottleneck is external tool calls or retrieval, Prompt Cache helps limitedly with end-to-end latency, but it can still reduce cost. The metrics need to be looked at separately.
The fifth pitfall is log leakage. To debug hit rates, logging the entire prompt might seem convenient, but it’s actually quite risky. A better approach is to log only the hash, version, token count, and a sanitized snippet from the sampled input.
IX. A Practical Checklist
If you’re planning to implement Prompt Cache in your team, proceed in this order.
- First, break the prompt into segments—don’t rush to enable caching yet.
- Tag each segment with its scope, version, and owner.
- Set up a key scheme that includes tenant, permissions, schema, and model family.
- In shadow traffic, log tokens, latency, hit candidates, and invalidation reasons.
- Only cache stable segments that have high hit potential and clear permission boundaries.
- Add a fallback so cache failures can't take down the main path.
- Add a control plane to support circuit breaking by app, tenant, or segment.
- After release, watch saved tokens, first token, and fallback rate—not just hit rate.
This work sounds more painful than "just add Redis," but what you get in return is explainability, rollback, and governance. Once an AI application goes to production, prompts increasingly look like a mix of code and config. And if code needs versioning, testing, canary rollout, and rollback, prompts deserve the same level of engineering treatment.
Conclusion
The point of Prompt Cache isn't to save a few tokens—it's to acknowledge that prompts have become a core asset of AI applications. Keep treating them as string concatenation, and your team will keep paying for it in cost, latency, and governance. Break them into stable segments, set up versioning protocols, isolation boundaries, and observability metrics, and AI applications finally have a shot at going from demo to reliable product.
If you take away just one thing, let it be this: don't cache "requests that look the same"—cache prompt assets that are semantically stable, safe at the boundaries, and version-controlled.
Source: Juejin · Weekly AI trends · juejin.cn