LLM Inference Cost Optimization 2026: A Three-Layer Caching Strategy
Stack prompt caching, semantic caching, and vLLM KV prefix caching into three layers to cut LLM inference cost by 40-90% — a 2026 production architecture with working code.
Contents
Why "inference cost" is now a survival variable for LLM products
If you have shipped an LLM feature to production, the first invoice probably gave you pause. Token cost is invisible in a demo, but the moment traffic arrives, inference becomes the single largest line in your infra budget. The 2026 reality is clear: producing the same result more cheaply matters more to your P&L than making the model smarter.
The key insight is that most requests are more similar than you think. In a support bot, internal search, or RAG Q&A, a handful of top questions account for over half the traffic, and the system prompt and document context are nearly identical across requests. Yet without caching, every request runs full inference — repeating the same computation thousands of times.
This article lays out a production architecture that stacks three layers — provider prompt (prefix) caching, semantic caching, and vLLM's KV prefix cache — to cut inference cost by 40-90%, with code. Because each layer catches a different kind of redundancy, stacking them beats using any one alone.
Background: what each of the three layers catches
Not all caching is the same. The three layers differ in which kind of redundancy they capture.
| Layer | Match basis | Redundancy caught | Savings | Cost on hit |
|---|---|---|---|---|
| Semantic cache | Embedding similarity (meaning) | Same question, different wording | 30-70% | Zero model call (~100%) |
| Prompt (prefix) cache | Exact token-prefix match | Identical system prompt / docs | 50-90% | Cache-read rate only |
| KV cache (self-host) | Token prefix + GPU KV | Multi-turn chat, long-doc requery | Higher throughput, lower latency | Skips GPU recompute |
The semantic cache recognizes "How do I get a refund?" and "tell me the refund process" as the same question and skips the model entirely. The prompt cache hits only when the token prefix is byte-for-byte identical — not semantically — but it cheaply handles the shared parts like system prompts and long documents. The KV cache reuses GPU-side KV tensors in a self-hosted engine (e.g. vLLM) to skip recomputing a shared prefix.
In the request path, traffic flows semantic cache → prefix cache → full inference; the higher it is stopped, the cheaper it is.
request ──▶ [1] semantic cache ── hit ──▶ return stored answer (0 model calls)
│ miss
▼
[2] prefix / prompt cache ── prefix hit ──▶ served at cache-read rate
│
▼
[3] full inference (vLLM KV cache absorbs some GPU recompute)
Deep dive: implementing each layer
Layer 1 — Semantic cache (meaning-based, avoids the model)
The biggest savings come from the semantic cache, which avoids calling the model at all. Embed the question, compare cosine similarity against existing cache entries, and return the stored answer if it clears the threshold. Below is a minimal implementation backed by Redis vector search.
# semantic_cache.py — embedding-similarity response cache
import numpy as np
from redis import Redis
from redis.commands.search.query import Query
# ⚠️ Note: the THRESHOLD governs cache quality.
# Too low → distinct questions treated as one (false hit) → wrong answer
SIM_THRESHOLD = 0.83 # cosine similarity start point (0.75-0.85 recommended)
TTL_SECONDS = 60 * 60 * 24 # match TTL to how often data changes
def lookup(redis: Redis, embed: np.ndarray) -> str | None:
"""Find the most similar entry; return its answer if above threshold."""
q = (
Query("*=>[KNN 1 @vec $v AS score]")
.sort_by("score")
.return_fields("answer", "score")
.dialect(2)
)
res = redis.ft("idx:cache").search(q, {"v": embed.astype(np.float32).tobytes()})
if not res.docs:
return None
doc = res.docs[0]
cosine = 1 - float(doc.score) # redis returns distance → convert to similarity
return doc.answer if cosine >= SIM_THRESHOLD else None
def store(redis: Redis, key: str, embed: np.ndarray, answer: str) -> None:
"""On a miss, store the (question embedding, answer) pair with a TTL."""
redis.hset(f"cache:{key}", mapping={"vec": embed.astype(np.float32).tobytes(),
"answer": answer})
redis.expire(f"cache:{key}", TTL_SECONDS)
The threshold is not a set-and-forget value. Monitor false-hit rate and hit rate in production and tune it. For a managed quick start, Redis LangCache handles embedding, storage, and similarity search via an API, with a default LANGCACHE_CACHE_THRESHOLD=0.65 you raise or lower to fit your FAQ style.
Layer 2 — Prompt (prefix) cache (provider API)
Commercial APIs (e.g. Anthropic) cache token prefixes shared across requests, such as system prompts and documents. With Anthropic, cache reads cost 10% of the base rate (a 90% discount) while cache writes carry a 25% premium over base input price — so you break even once an identical prefix is reused 2+ times. Put the invariant parts (system instructions, long documents) up front and mark the cache point with cache_control.
# prompt_cache.py — Anthropic prompt caching (reuse a long-doc prefix)
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=[
{"type": "text", "text": "You are IntentCode's internal-docs assistant."},
{
# ⚠️ Cache point: this block (long manual) is identical per request → cache-read rate
"type": "text",
"text": LONG_MANUAL_TEXT, # a fixed document of tens of thousands of tokens
"cache_control": {"type": "ephemeral"},
},
],
messages=[{"role": "user", "content": "Summarize the refund policy in 3 lines"}],
)
# Track savings via usage.cache_read_input_tokens
print(resp.usage)
Track cost via usage.cache_read_input_tokens in the response — the larger it is, the better your prefix design.
Layer 3 — vLLM KV prefix cache (self-host)
If you serve the model on your own GPUs, vLLM's Automatic Prefix Caching (APC) reuses the KV cache. A new request sharing a prefix skips recomputing the shared part, raising throughput and cutting latency on long-document requeries and multi-turn chat. Enabling it is one engine flag.
# Start the vLLM server — enable Automatic Prefix Caching
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-prefix-caching \
--max-model-len 8192 \
--gpu-memory-utilization 0.90
# Or enable it directly from the Python engine
from vllm import LLM
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
enable_prefix_caching=True, # ⚠️ KV reuse — pays off for shared system prompts/docs
gpu_memory_utilization=0.90,
)
Practical guide: wiring three layers into one request path
Chain the three layers into a single request path. Stop and return early at the higher layers; only requests that pass flow downward.
# pipeline.py — semantic → prefix → full inference, in order
async def answer(question: str) -> str:
embed = await embed_query(question) # 1) embed the question
cached = lookup(redis, embed) # 2) semantic cache
if cached:
metrics.inc("cache.semantic.hit")
return cached # zero model calls
# 3) LLM call with prompt caching on (long-doc prefix at cache-read rate)
answer = await call_llm_with_prefix_cache(question)
store(redis, key=hash_q(question), embed=embed, answer=answer) # 4) populate cache
metrics.inc("cache.semantic.miss")
return answer
An operational checklist: first, start the semantic threshold near 0.83 and tune by watching false hits. Second, match TTL to data change frequency (short for fast-changing prices/inventory). Third, place fixed prefixes (system instructions, docs) up front and variable parts (user question) at the end to raise prefix hit rate. Fourth, keep hit rate, savings, and false hits on a live dashboard.
Troubleshooting: three common field issues
① The semantic cache returns wrong answers (false hits). The threshold is too low or the embedding model is weak, mapping semantically distinct questions to nearby vectors. Raise the threshold (e.g. 0.83 → 0.88) and swap in a stronger embedding model. Cache quality depends on embedding-model choice more than any other knob.
② Prompt caching is on but cost did not drop. Variable data (timestamps, usernames) is mixed into the front of the prefix, so the prefix differs per request. Move fixed blocks to the very front and push variable values to the back. Also, if an identical prefix is reused fewer than twice, the write premium makes it a net loss — exclude low-reuse prefixes from caching.
③ GPTCache slows down under concurrent writes. GPTCache defaults to a SQLite backend that scales poorly under concurrent write load. In production, switch the backend to Redis or PostgreSQL. A single-node SQLite setup taking live traffic is a classic bottleneck.
Conclusion + the IntentCode view
LLM inference cost is not a "swap the model" problem but a design problem of "not repeating the same computation." Strip out meaning-level redundancy with the semantic cache, prefix redundancy with prompt caching, and GPU recompute with the KV cache, and you can cut cost by 40-90% without hurting quality.
In our MLOps and RAG lines, IntentCode applies this three-layer caching as a default architecture, so a client's LLM service gets cheaper per unit as traffic grows. Translating intent into cost-efficient code is what we do.