(Repo url)
LLM applications have an unusual relationship with repetition.
Users repeatedly ask:
How do I reset my password?Then they ask:
I forgot my password. What should I do?Followed by:
Can you help me regain access to my account?To a conventional cache, these are three unrelated strings.
To a human, they are the same support ticket wearing different hats.
If every version travels through retrieval, prompt assembly, a hosted model, and post-processing, the application repeatedly pays for roughly the same answer
This is less a sophisticated AI architecture and more a subscription service for déjà vu
This is the problem addressed by Semacache, a Python library that combines exact-key caching with semantic similarity lookup for LLM applications.
Repository:
https://github.com/uncle-voh-max/semacacheSemacache places a dual-layer cache in the query path
-
Look for the exact same request.
-
If that misses, look for a semantically similar request.
-
Only invoke the expensive generator when both miss.
** The library supports in-memory and Redis-backed stores, configurable embeddings, tenant isolation, cache policies, request coalescing, validation, health checks, invalidation, and metrics.
Why caching belongs in the LLM query path
Caching is hardly new.
We cache database records, HTTP responses, rendered pages, DNS lookups, and occasionally the answer to “what did product actually ask us to build?”
Traditional caches normally depend on exact identity:
same key → same resultThat works well for deterministic requests such as:
GET /customers/123LLM requests are different because users express equivalent intent using different language.
Consider:
A: What is semantic caching?
B: Explain semantic caching.
C: How does a cache based on meaning work?An exact cache can reuse the response for another occurrence of A.
A semantic cache may reuse the response across A, B, and C : provided their embeddings are similar enough and their surrounding metadata is compatible.
This gives us two complementary cache layers.
Exact cache
The exact cache uses a deterministic key, such as a SHA-256 hash derived from the query and relevant metadata.
It is:
-
fast
-
cheap
-
predictable
-
appropriate for byte-for-byte or normalised repetitions
Semantic cache
The semantic cache embeds the incoming query and compares it with cached query embeddings.
It is
-
more flexible
-
slightly more expensive than exact lookup
-
much cheaper than a full LLM generation
-
sensitive to similarity thresholds and scope
The architectural principle is straightforward:
Use the cheapest reliable answer source first.
Do not generate an embedding when an exact hash lookup will do. Do not call a large model when an existing semantically equivalent answer is safe to reuse.
The Semacache query path
Semacache organises its implementation into a client layer, an orchestration service, and pluggable protocol-backed components for exact stores, semantic stores, embeddings, validation, eviction, coalescing, and metrics.
The effective query path looks like this

The repository’s architecture follows this ordering: exact lookup first, then embedding and semantic lookup, followed by generator execution and storage after a miss
That order matters.
If semantic search happened first, every query, including exact duplicates would pay the embedding cost.
A good cache should not perform a small expensive operation to discover that a free operation would have worked
Getting started without constructing a cache empire
For local development, Semacache provides an in-memory constructor:
from semacache import CacheMetadata, CachePolicy, SemanticCache
async with SemanticCache.in_memory(
namespace="support-bot",
ttl_seconds=3600,
) as cache:
result = await cache.get_or_generate(
query="How do I reset my password?",
generator=generate_answer,
metadata=CacheMetadata(agent="support"),
policy=CachePolicy.READ,
)
print(result.status, result.value)The first request is a miss and invokes generate_answer.
A repeated query becomes an exact hit.
A sufficiently similar query may become a semantic hit
result = await cache.get_or_generate(
query="I forgot my password. How can I regain access?",
generator=generate_answer,
metadata=CacheMetadata(agent="support"),
policy=CachePolicy.READ,
)The primary API returns a typed result containing the value, cache status, semantic similarity where relevant, and execution information.
This is a useful API shape because callers should know whether a response was:
EXACT_HIT
SEMANTIC_HIT
MISS
DISABLEDA cache that hides its decisions is easy to adopt and difficult to operate.
Sensible defaults: useful, conservative, and replaceable
Defaults are part of a library’s architecture.
A poor default pushes every caller towards accidental complexity. A reckless default quietly trades correctness for performance and waits for production to notice.
Semacache starts with few defaults:
TTL: 3600 seconds
Maximum entries: 10,000
Estimated memory ceiling: 512 MB
Eviction: TTL + LRU
Semantic caching: enabled
Similarity threshold: 0.85
Similarity metric: cosine
Embedding provider: Ollama
Embedding model: nomic-embed-text
Request coalescing: enabled
Concurrent LLM calls: 5
Request timeout: 120 seconds
Reject empty responses: true
Cache-read failure: bypassThese defaults are published in the repository’s configuration reference.
Request coalescing
Suppose 100 identical requests arrive immediately after an entry expires.
Without coalescing:
100 cache misses → 100 LLM requestsWith coalescing:
100 cache misses → 1 LLM request + 99 waitersSemacache enables coalescing by default and supports memory-based or Redis-backed coordination.
This protects the system from a cache stampede, sometimes known in meetings as “unexpected organic growth.”
Validation before storage
Semacache rejects empty responses by default and provides a response-validation extension point.
This is an underrated feature.
Without validation, a transient model failure can be cached and served repeatedly:
First request:
LLM returns empty string
Next 10,000 requests:
Cache efficiently returns empty stringCaching amplifies both good and bad results.
Validate before storing:
-
response is not empty
-
expected schema is valid
-
required citations exist
-
safety checks passed
-
result is not an error page
-
result has an acceptable quality score
Configuring Semacache for production
A distributed configuration can use Redis for both cache layers and Ollama for embeddings:
from semacache import SemanticCache, SemanticCacheConfig
config = SemanticCacheConfig(
namespace="customer-support",
ttl_seconds=3600,
exact_cache={
"provider": "redis",
"url": "redis://localhost:6379/0",
},
semantic_cache={
"provider": "redis",
"enabled": True,
"similarity_threshold": 0.90,
},
embeddings={
"provider": "ollama",
"model": "nomic-embed-text",
},
coalescing={
"enabled": True,
"mode": "redis",
"lock_timeout_seconds": 30,
},
max_concurrent_llm_requests=5,
request_timeout_seconds=120,
)
cache = SemanticCache(config=config)The same configuration can be loaded from YAML:
namespace: customer-support
ttl_seconds: 3600
exact_cache:
provider: redis
url: redis://localhost:6379/0
semantic_cache:
provider: redis
enabled: true
similarity_threshold: 0.90
embeddings:
provider: ollama
model: nomic-embed-text
coalescing:
enabled: true
mode: redis
lock_timeout_seconds: 30Or through SEMACACHE_* environment variables. The repository supports all three configuration styles.
Senior-engineering tip: Put operational values in configuration, but keep policy ownership explicit. A threshold living in YAML is configurable, a threshold nobody owns is merely abandoned
Scope is part of the cache key
Semantic equivalence is not enough.
The same question may require different answers for:
-
different tenants
-
different users
-
different agents
-
different models
-
different permissions
-
different geographic regions
Semacache includes metadata fields for tenant, user scope, model, agent, and custom values:
metadata = CacheMetadata(
tenant="acme-corp",
user_scope="user-123",
model="gpt-4.1-mini",
agent="support-bot",
extra={
"locale": "en-AU",
"knowledge_version": "2026-08",
},
)The repository builds multi-tenant and user-level isolation into its metadata model.
This is critical.
The following queries may be semantically identical
What is my current balance?
What amount do I have available?They must never share an answer across users.
Similarity tells us that two questions mean the same thing.
It does not tell us that two callers are authorised to receive the same data.
Cache policies for different operational needs
Semacache exposes four policies.
READ
Use the cache normally. Read existing entries, call the generator on a miss, and store the result.
policy = CachePolicy.READThis is the normal production path.
BYPASS
Call the generator directly without reading or writing the cache.
policy = CachePolicy.BYPASSUseful for debugging, experiments, and queries that must always be fresh.
REFRESH
Generate a new response and update the cache.
policy = CachePolicy.REFRESHUseful after content updates or when a caller explicitly requests freshness.
DISABLED
Skip cache behaviour entirely.
policy = CachePolicy.DISABLEDThe repository documents these policies as part of its public API.
A common pattern is to select policy from request context
policy = (
CachePolicy.REFRESH
if request.force_refresh
else CachePolicy.READ
)Avoid scattering cache bypass conditions through business code. Centralise the decision so that nobody has to reverse-engineer why one endpoint has twelve mysterious if not cache branches.
What should not be semantically cached?
Semantic caching is most valuable when answers are reusable.
Strong candidates include:
FAQs
Documentation assistance
Product explanations
Policy summaries
Static code examples
General onboarding
Repeated classification requestsPoor candidates include:
Personalised account data
Rapidly changing prices
Current inventory
One-time security decisions
Nondeterministic creative output
Requests containing unique tool results
Prompts whose system instructions differ materiallyYou should also be cautious when query wording carries subtle meaning
Consider:
Can I cancel the transfer?
How can I cancel the transfer?
Why can’t I cancel the transfer?These are nearby in embedding space, but not necessarily interchangeable.
The first asks about eligibility.
The second asks for procedure.
The third may indicate a failed or irreversible state.
A lower threshold may collapse all three into one cheerful but incorrect answer.
Evaluating the cache before celebrating the savings
A cache hit is not automatically a good outcome.
An exact hit is usually easy to reason about.
A semantic hit must be evaluated along two dimensions:
Efficiency: Did it avoid expensive work?
Correctness: Was the cached answer genuinely reusable?The evaluation flow should look like this:

The minimum useful metrics are:
Hit rate
(exact hits + semantic hits) / total requestsSemantic precision
correct semantic hits / all semantic hitsLLM avoidance rate
requests served without generation / total requestsCost reduction
baseline generation cost − cached-path costLatency reduction
Compare p50, p95, and p99 latency before and after enabling the cache.
Stale or incorrect response rate
Track semantic hits later rejected by users, validators, or downstream checks.
Do not optimise only for hit rate.
A cache with a 95% hit rate and a 20% incorrect-reuse rate is not efficient. It is confidently distributing leftovers.
Estimating expected cost reduction
Consider an illustrative workload of 100,000 monthly queries.
Assume:
Average LLM request cost: $0.012
Average embedding cost: $0.0001
Exact-cache hit rate: 20%
Semantic-cache hit rate: 35%
Miss rate: 45%Without caching:
100,000 × $0.012 = $1,200With dual-layer caching:
Exact hits:
20,000 × approximately $0 = approximately $0
Semantic lookup candidates:
80,000 × $0.0001 = $8
LLM generations:
45,000 × $0.012 = $540
Approximate total:
$548Estimated reduction:
$1,200 − $548 = $652 per monthThat is roughly a 54% reduction in this illustrative example.
The real result will depend on traffic repetition, prompt size, model price, embedding cost, TTL, threshold, and correctness requirements.
The expected evaluation shape is:
More exact hits
→ almost pure savings
More correct semantic hits
→ substantial savings with small embedding overhead
More false semantic hits
→ impressive dashboard, unhappy users
Higher threshold
→ fewer but safer semantic hits
Lower threshold
→ more reuse, greater correctness riskFailure policies should protect the primary path
Semacache allows cache-read failures to bypass the cache, write failures to be logged or raised, and embedding failures to raise or return no result.
A sensible production philosophy is
Cache unavailable:
Prefer degraded performance over total outage
LLM unavailable:
Return a deliberate error or approved stale response
Cache write fails:
Serve the valid generated answer and record the failureThe cache exists to improve the system.
It should not become a more fragile dependency than the expensive service it protects.
Whether to serve stale data should depend on the domain. The repository defaults
serve_staleto false, which is the safer general-purpose choice.
Observe outcomes, not merely cache size
Semacache exposes statistics, health checks, execution information, and a metrics-recorder extension point. Its metrics protocol includes cache hits, misses, generator latency, embedding requests, and errors.
Track:
Exact_hit_rate
Semantic_hit_rate
Semantic_similarity_distribution
Cache_miss_rate
Generator_latency
Embedding_latency
Cache_read_failures
Cache_write_failures
Coalesced_request_count
Response_validation_failures
Estimated_cost_avoidedBreak these down by:
-
tenant
-
model
-
route
-
cache namespace
-
application version
A global 50% hit rate can hide one tenant receiving 90% incorrect reuse while another gets none.
Aggregates are useful. Segmentation is where the bodies are found.
Closing thought
Traditional caching asks:
Have I seen this key before?
Semantic caching asks:
Have I answered this meaning before?
That second question can dramatically reduce LLM latency and cost, but it introduces judgement into the cache path.
Semacache handles the mechanics thoughtfully:
-
exact lookup before embeddings
-
semantic lookup before generation
-
explicit policies
-
response validation
-
request coalescing
-
tenant-aware metadata
-
pluggable stores and embedding providers
-
observable cache outcomes
The remaining responsibility belongs to the integrating team:
-
define what may be reused
-
evaluate semantic precision
-
tune thresholds against real queries
-
invalidate when knowledge changes
-
measure both cost savings and incorrect hits
The senior-engineering lesson is simple:
A semantic cache is not merely a performance optimisation. It is a controlled decision to reuse a previous answer.
Make that decision cheap.
Make it visible.
And most importantly, make sure it is correct before proudly announcing that the system has saved 54% by answering the wrong question faster.