Reduce AI latency and improve responsiveness by integrating Azure Cache for Redis with LLM workloads
Slashing Latency: Deploying an Azure Cache for Redis
Gateway in Front of Your LLM Endpoints
"What's your return policy?" and "How do returns work?" are the same question wearing different clothes. An exact-match cache sees two different strings and calls the model twice. A semantic cache sees two vectors four degrees apart and answers the second one in under 200 milliseconds, for free. The gap between those two outcomes is one architectural decision — and one Azure tier most teams don't realize they need.
# A support chatbot's actual traffic pattern over one hour, deduplicated
# by MEANING rather than by exact string match:
Query | Count | LLM calls today
------------------------------------------------|-------|------------------
"What's your return policy?" | 1 | 1
"How do returns work?" | 1 | 1 <- SAME QUESTION
"Can I return an item I bought last week?" | 1 | 1 <- SAME QUESTION
"What's the process to send something back?" | 1 | 1 <- SAME QUESTION
... (41 more phrasings of the identical question)
TOTAL exact strings: 44 unique -> 44 LLM calls today
TOTAL semantic clusters: 1 real question -> should be ~1 LLM call
+ 43 cache hits
# Cost of the naive approach, this ONE question, this ONE hour:
44 calls x (~180 input tokens + ~220 output tokens) x $10/1M output
+ $2.50/1M input (GPT-4o Global Standard, illustrative rates)
= 44 calls, full latency (800ms-2.5s each) and full price, EVERY time,
for a question the system had already answered 43 times before.
# What an EXACT-match cache (e.g. plain Redis GET/SET on the raw prompt
# string) does with this traffic: essentially nothing. Zero cache hits,
# because none of the 44 strings are byte-for-byte identical.
# What semantic caching resolves: query #2 embeds to a vector 0.03
# cosine-distance from query #1's stored vector -> HIT -> 180ms response,
# zero LLM tokens spent, zero backend load added.Symptom: Repetitive, high-volume LLM traffic where the same underlying question arrives in dozens of different phrasings, each treated as a fresh, uncached request. Failure point: Caching implemented (if at all) as exact key-value matching on the raw prompt string, which cannot recognize that two differently-worded questions mean the same thing. Default platform behaviour: Neither Azure OpenAI nor a plain cache layer deduplicates by meaning. Nothing before this fix distinguishes "the same question, reworded" from "a genuinely new question" — every request is treated as unprecedented.
Most repetitive LLM traffic isn't repetitive in the way a plain cache can see. Users don't type identical strings — they type the same intent in dozens of different words, and a key-value cache keyed on the raw prompt treats every one of those phrasings as a brand-new question, calling the model, paying full token price, and absorbing full latency every single time. A semantic cache fixes this by comparing meaning instead of text: it embeds the incoming query into a vector, searches a vector index of previously-answered questions for the nearest neighbor, and if that neighbor is close enough, returns the stored answer without ever touching the LLM. Azure Cache for Redis is a genuinely strong platform for this — but only the tier of it that actually supports vector search, and only once you understand a scoring convention that is, on its face, backwards from what most engineers assume.
This is the single most important thing to get right before writing any code, because getting it wrong means discovering the whole architecture is impossible only after you've already provisioned the wrong resource. Semantic caching depends entirely on vector similarity search — finding the nearest previously-cached question to an incoming query — and in Redis, that capability comes from a specific module: RediSearch. Microsoft's own documentation is explicit: RediSearch, including its vector similarity search features, is available only on the Enterprise and Enterprise Flash tiers of Azure Cache for Redis.
| Tier | RediSearch / vector search | Can build semantic caching? |
|---|---|---|
| Basic | Not available | No |
| Standard | Not available | No |
| Premium | Not available | No |
| Enterprise | Available | Yes |
| Enterprise Flash | Available (RediSearch in preview) | Yes |
This isn't a minor feature gap — it's a hard architectural wall. A Standard or Premium cache, however large or well-configured, simply has no vector index type to create, no HNSW algorithm to query, and no way to perform the "find the nearest previously-answered question" operation that the entire pattern depends on. If your team already has a Premium-tier cache in production for ordinary caching duties, that instance cannot be repurposed for semantic caching — it needs a separate Enterprise-tier deployment, or a migration.
When provisioning the Enterprise tier specifically for RediSearch, the Clustering Policy must be set to Enterprise — not OSS Cluster. RediSearch is only supported under the Enterprise cluster policy. This is a setting chosen at cache creation time in the portal or CLI, easy to get wrong if you're following generic Redis provisioning habits from a non-search use case, and it isn't something you can change after the fact without recreating the cache.
Microsoft's more recent guidance increasingly points to Azure Managed Redis (AMR) — a newer, fully-managed first-party Redis offering — as the forward path for these AI-adjacent scenarios, including semantic caching, vector stores, and conversation memory. Functionally, for this article's purposes, the requirement is the same: vector search capability requires the tier that includes RediSearch. If you're standing up new infrastructure rather than working with an existing Azure Cache for Redis deployment, check current documentation for whether Azure Managed Redis or Azure Cache for Redis Enterprise is the currently-recommended resource type, as Microsoft's naming and default recommendations in this space have been evolving.
The mechanism is straightforward once the tier requirement is satisfied. Every incoming query goes through the same three-step check before it's allowed anywhere near the LLM.
| Step | What happens | Cost |
|---|---|---|
| 1. Embed | The incoming query text is converted to a vector via an embeddings model call (a separate, cheap deployment — not the completions model) | Small — one embedding call, always incurred, hit or miss |
| 2. Search | Redis's HNSW-indexed vector search finds the nearest previously-cached query vector, using cosine distance (or another configured metric) | Sub-millisecond at Redis's typical scale for this use case |
| 3. Decide | If the nearest match's distance is below the configured threshold, return its cached answer. Otherwise, proceed to the LLM and cache the new result | Zero (hit) or full LLM cost (miss) |
The economics only work because Step 1's embedding call is dramatically cheaper than a full chat completion — a small, fast embeddings model call costs a small fraction of what a GPT-4o-class completion costs, and every cache hit saves the entire completion cost while paying only the embedding cost. Even a modest hit rate compounds quickly on high-volume, repetitive traffic like FAQ bots, internal support tools, and customer-facing chat with predictable question patterns.
A detail worth internalizing: the embedding step runs on every query, hit or miss — you have to embed the query to know whether it matches anything cached. This means semantic caching adds a small, fixed cost and a small, fixed latency (typically tens of milliseconds) to every single request, in exchange for potentially eliminating the much larger cost and latency of a full completion call on a hit. For workloads with very low repeat-question rates, this fixed overhead without a corresponding high hit rate can make semantic caching a net negative — know your traffic's actual repetition pattern before assuming this pays for itself.
This is the detail that trips up almost everyone building or configuring a semantic cache for the first time, because the name score-threshold invites exactly the wrong intuition. It sounds like a similarity score — the kind where higher means "more similar," the way a percentage match or a relevance score usually works. It is the opposite: score-threshold is a distance value, where smaller numbers mean the query has to be closer — more similar — to produce a cache hit.
| score-threshold value | What it means | Practical effect |
|---|---|---|
| 0.01 – 0.05 | Aggressive — only very close paraphrases match | High-confidence hits; safe default for most production workloads |
| 0.05 – 0.20 | Conservative — looser matching, wider net | More hits, but rising risk of matching genuinely different questions |
| Above 0.20 | Explicitly flagged by Microsoft as risky | "May lead to cache mismatch" — serving a wrong answer with false confidence |
Microsoft's own APIM policy documentation states the recommended starting point directly: begin with a low value such as 0.05, and adjust from there to balance the ratio of cache hits to misses. A score threshold above 0.2 may lead to cache mismatches. If your intuition says "I want a stricter cache, I should raise the threshold" — resist it. Raising the number loosens the match; lowering it tightens the match.
Two embeddings of genuinely paraphrased sentences — "What's your return policy?" and "How do returns work?" — will typically land at a cosine distance in the very low range, often under 0.05. Two embeddings of superficially similar but semantically different questions — "What's the weather in Paris?" and "What's the weather in Berlin?" — sit much further apart despite sharing most of their words, because the model has correctly separated the two cities as distinct entities. A well-chosen low threshold catches the first pair as a hit and correctly rejects the second as a miss. A threshold set too high, chasing "more matches," starts blurring exactly this kind of distinction.
Architectural Topology: Failing vs Remediated
| Layer | Failing configuration (current) | Remediated configuration (fix) |
|---|---|---|
| Cache tier | Basic/Standard/Premium — no vector search capability at all | Enterprise or Enterprise Flash, with Enterprise clustering policy |
| Cache mechanism | Exact-match key-value (if any) — misses every paraphrase | Vector similarity search via RediSearch (HNSW index) |
| Matching logic | None, or naive string comparison | Cosine distance against a tuned score-threshold |
| Threshold intuition | Assumed "higher = stricter" (backwards) | Correctly understood as distance — lower = stricter, verified against real query pairs |
| Embeddings backend | Reuses the completions deployment, or missing entirely | Dedicated, separate embeddings deployment |
| Cross-user isolation | Global cache, no partitioning — risk of leaking one user's cached answer to another | vary-by partitioning on subscription/user identity |
| Wrong-answer safeguard | None — any hit above threshold returned unconditionally | Threshold tuned conservatively, monitored, with explicit override paths for sensitive queries |
| Build vs buy | Not evaluated — assumed custom build is the only option | APIM's built-in policy considered as the lower-effort default |
Everything downstream depends on getting this step right. Provision an Enterprise-tier cache with the Enterprise clustering policy, and enable the RediSearch module explicitly.
Don't assume the module enabled successfully — verify it. Connect with a Redis client and run MODULE LIST; the response should include search among the loaded modules. If it doesn't appear, the database wasn't created with the module correctly specified, and no amount of correct application code will make vector search work against it — the fix is at the resource level, not the client code.
This is the core implementation the brief describes: a gateway layer that embeds every incoming query, checks Redis for a semantically close prior answer, and only calls Azure OpenAI on a genuine miss.
The example above uses a Redis access key for brevity, but the same "no standing secret" principle from managed-identity migrations applies here: Azure Cache for Redis Enterprise supports Microsoft Entra ID authentication as an alternative to access keys. For a production gateway, authenticate the Redis connection the same way you'd authenticate to Azure OpenAI — via a managed identity — rather than embedding a long-lived Redis key in configuration.
Before committing to the custom gateway in Section 6, know that Microsoft ships an officially supported, lower-effort alternative if Azure API Management already sits in front of your Azure OpenAI endpoints: the azure-openai-semantic-cache-lookup and azure-openai-semantic-cache-store policies (with generic llm-semantic-cache-* equivalents for non-Azure or AI Model Inference API backends) implement essentially the same pattern as configuration, not code.
| Aspect | Custom gateway (Section 6) | APIM built-in policy |
|---|---|---|
| Effort | Full application code, index management, error handling | Configuration only — no custom code to maintain |
| Flexibility | Full control — custom key strategy, multi-model routing, bespoke logic | Limited to what the policy exposes (threshold, vary-by, message filtering) |
| Prerequisite | Enterprise-tier Redis + application hosting | APIM already fronting your Azure OpenAI APIs + Enterprise-tier Redis as external cache |
| Maintenance | You own the gateway code, upgrades, and bug fixes | Microsoft maintains the policy implementation |
| Best fit | Non-APIM architectures, or needing logic APIM's policy doesn't support | Any architecture already using APIM as the AI gateway — the default recommendation |
The APIM path still requires the same Enterprise-tier Azure Cache for Redis with RediSearch enabled, configured as APIM's external cache resource, plus a dedicated embeddings backend registered separately from your completions backend. The policy doesn't remove the tier requirement from Section 1 — it removes the need to write and maintain the gateway application code that implements the lookup/store logic yourself.
Given that the underlying mechanism and infrastructure requirements are identical, the practical recommendation for most teams already running APIM in front of Azure OpenAI is to start with the built-in policy. It gets you the exact cost and latency benefit the brief describes with a fraction of the engineering investment. Reach for the custom gateway from Section 6 only when you hit something the policy genuinely can't do — custom eviction logic, multi-tenant routing more complex than vary-by supports, or an architecture where APIM isn't in the request path at all.
Microsoft's own policy documentation states the risk plainly: because semantic caching returns responses based on similarity rather than exact match, it can surface responses that are incorrect, outdated, or unsafe for the current request. This isn't a footnote — it's a design constraint that shapes how the cache must be partitioned and tuned.
Cross-user leakage: partition the cache
A global, unpartitioned cache means any user's semantically-similar query can retrieve any other user's previously-cached response — including responses that referenced account-specific, personal, or otherwise sensitive information in their original context. The fix is vary-by: partition the cache by subscription ID, authenticated user ID, or tenant, so a cache hit can only ever return a response that was generated for a matching scope.
For a custom gateway, implement the same partitioning by scoping the Redis key prefix or the vector search filter to the authenticated caller's identity, so the search only ever considers cache entries within that scope.
Wrong-answer risk: tune conservatively, monitor continuously
- Start at the documented conservative default (0.05) and only loosen it after measuring real false-positive rates against your specific query distribution — don't guess your way to a looser threshold to chase a higher hit rate.
- Log every cache hit with the matched query, the incoming query, and the distance score, so you can audit for near-misses that shouldn't have matched.
- For high-stakes domains (medical, legal, financial guidance), consider excluding semantic caching entirely, or requiring an even stricter threshold than general-purpose traffic.
- Set a TTL on cached entries (Section 6's example uses 86,400 seconds / 24 hours) so stale answers naturally expire rather than persisting indefinitely as source information changes.
Both APIM's policy (ignore-system-messages="true") and a custom implementation should exclude system prompts from the similarity computation. System messages are often identical or near-identical across many different user queries within the same application, and including them in the embedded text dilutes the signal — two genuinely different user questions can appear artificially similar if a long, shared system prompt dominates the embedding. Embed only the user-facing conversational content that actually varies per query.
Semantic caching's failure modes are quiet — a wrong cached answer looks exactly like a right one to the user receiving it, which is what makes getting the configuration wrong more dangerous than a system that fails loudly.
| Anti-pattern | Why it feels right | Why it isn't |
|---|---|---|
| Provision Premium tier "because it's cheaper than Enterprise" | "We don't need the fanciest tier for a cache" | Premium has no vector search at all. This isn't a cost-optimization choice — the whole pattern is structurally impossible on that tier |
| Raise score-threshold to "loosen" the cache | "Higher number sounds like more permissive" | Backwards. Raising the threshold makes matching stricter... no, raising it makes it LOOSER but riskier — the naming is genuinely counterintuitive either way you phrase the intuition. Verify with real query pairs, don't rely on the name alone |
| Global cache, no vary-by, "we'll add partitioning later" | "Ship the cost savings first, security later" | Cross-user leakage is a data exposure risk from day one, not a future concern. Partition before any production traffic, not after |
| Reuse the completions model deployment for embeddings | "One less resource to manage" | Different model classes for different jobs. A dedicated, cheap embeddings deployment keeps the cost/latency math favorable and avoids capacity contention with completion traffic |
| No TTL on cached entries | "More cache hits forever, why expire anything" | Source information changes. A permanently-cached wrong answer to a since-updated policy question is worse than no cache at all |
| Skip monitoring cache-hit accuracy after launch | "It passed testing, we're done" | Real production query distributions differ from test data. A threshold tuned on synthetic examples can behave differently at scale — monitor continuously, not just at launch |
Validation & Verification: Confirm the Fix
Confirm the cache hits on genuine paraphrases, correctly misses on superficially similar-but-different questions, and doesn't leak across users — in that order, before trusting it in production.
Four conditions must hold together. One: genuine paraphrases of the same question reliably produce cache hits, verified against real pairs, not assumed from the threshold value alone. Two: superficially similar but semantically different questions reliably produce misses — the adversarial test, not just the happy-path test. Three: cross-user isolation is verified directly, by attempting to retrieve one user's cached answer as a different user and confirming it fails. Four: the measured hit rate and cost savings against real production traffic are tracked and compare favorably to the pre-build cost/benefit estimate. Miss any of the four and you either haven't captured the savings the brief promises, or you've built a system that occasionally, silently, confidently gives someone the wrong answer.
Key Takeaways
Frequently Asked Questions
Related FAVRITE Articles
- The Shared Tenant Noise Performance Drop: Diagnosing Noisy-Neighbor Latency
- Splitting the Bill: Isolating Semantic Ranker Costs from Agentic Retrieval Plans
- The PTU Math Trap: When to Pivot from Pay-As-You-Go to Provisioned Throughput
- Strict Isolation: Document-Level Security in Azure AI Search for Multi-Tenant Apps