Skip to content

Cost and FinOpsMethodology

Is semantic caching safe for LLM apps? Hit rates, risks, tests

Semantic caching answers a new prompt with the stored answer to a similar one. That can cut provider calls, but it can also return a wrong or another tenant's answer. This is a method to measure the false-hit rate, pick a threshold and scope the key before any traffic depends on it.

By INFRO Editorial

Published

9 min read

Glowing token streams being compressed into a precise efficient AI infrastructure pipeline

In short

Semantic caching is safe only when the cache key is scoped by tenant, policy and model before similarity is computed, and when you have measured a false-hit rate per similarity band on your own traffic. Run it in shadow mode first, and compare the marginal saving against provider prompt caching, not against a cold call.

Key takeaways

  • Scope the cache key by tenant, policy, model and prompt version as hard filters, and only then compare embeddings inside that scope.
  • Measure false-hit rate per similarity band on labeled pairs from your own traffic, and report an upper confidence bound, not the point estimate.
  • Run the cache in shadow mode first: compute would-be hits, still call the provider, and compare the two answers.
  • Price the saving against a request that already benefits from provider prompt caching, because a cached-prefix read is already a fraction of an input token.
  • Treat a cross-tenant hit as a security incident, not a quality defect.

What semantic caching adds, and what it risks

An exact-match cache returns a stored answer only when everything that decides the answer is identical. INFRO's response cache works this way: the same organization, project and key environment, the same model or route, the same guardrail policy, the same messages and the same generation parameters, and it says plainly that there is no semantic matching.1 A semantic cache relaxes the "same messages" part. It embeds the prompt, finds the nearest stored prompt, and returns that prompt's answer if the similarity score clears a threshold.

That relaxation is where all the risk lives. Exact matching can only be wrong by being stale. Similarity matching can also be wrong by being different: "Can I cancel after 30 days?" and "Can I cancel within 30 days?" embed very close together and have opposite answers. And if the scope of the lookup is wider than it should be, the wrong answer can belong to another customer.

So the question "is it safe?" splits into three tests you can run before shipping:

  1. Does the key keep tenants and policies apart? (a design check)
  2. At the threshold you plan to use, how often is a hit wrong? (a measurement)
  3. After provider prompt caching, is the remaining saving worth the first two? (arithmetic)

The rest of this article is a procedure for each.

Step 1: scope the key before you compare vectors

Similarity should only ever rank candidates inside a scope that is matched exactly. Never put tenant identity into the embedding text and hope the vector separates it. Treat these as hard filters applied before the nearest-neighbor search:

  • Tenant or end customer. Different customers get different cache partitions, always.
  • Policy. The guardrail policy in force, and any allowlist or compliance setting that changes what an answer may contain. INFRO's exact-match key includes the guardrail policy for this reason.1
  • Model or route. An answer from one model is not a stand-in for another, and a silent model swap changes the answer distribution.
  • System prompt and tool set version. Hash them. A prompt edit must invalidate the partition.
  • Generation parameters. Temperature, response format, max tokens.
  • Retrieved context identity. If the answer depends on retrieved documents or user data, the document ids and their access-control scope belong in the key. Otherwise a hit replays an answer built from documents the new caller may not be allowed to read.
  • Locale and language, if the answer text depends on them.
cache_scope.py
import hashlib, json

def scope_key(*, tenant_id, policy_id, model, system_prompt, tools,
              params, context_ids, locale):
    material = {
        "tenant": tenant_id,
        "policy": policy_id,
        "model": model,
        "sys": hashlib.sha256(system_prompt.encode()).hexdigest(),
        "tools": hashlib.sha256(json.dumps(tools, sort_keys=True).encode()).hexdigest(),
        "params": params,            # temperature, response_format, max_tokens
        "ctx": sorted(context_ids),  # retrieved doc ids the answer depends on
        "locale": locale,
    }
    blob = json.dumps(material, sort_keys=True).encode()
    return hashlib.sha256(blob).hexdigest()

# Nearest-neighbor search runs only over entries stored under this exact key.

OWASP's 2025 list names Sensitive Information Disclosure and Vector and Embedding Weaknesses as separate risks for LLM applications.2 A shared vector index across tenants sits on both. Treat any hit that crosses a tenant partition as a security incident with its own alert, not as a quality defect, and test it directly: seed tenant A with a distinctive prompt and answer, send the near-identical prompt as tenant B, and assert a miss.

Also decide what is never eligible. INFRO's exact cache already refuses to store truncated or filtered answers (length, content_filter), responses that output guardrails modified, and requests with images behind a URL, because the content behind a URL can change.1 Copy those exclusions, and add your own: tool-calling turns with side effects, answers that quote user-specific data, and anything non-deterministic where variety is the point.

Step 2: measure the false-hit rate by similarity band

A hit rate alone tells you how often the cache fires. It says nothing about whether it fired correctly. The number that matters is the false-hit rate: of the requests answered from the cache, the share where the cached answer would not have been acceptable for the new prompt.

Build the measurement from your own traffic, since embedding models, domains and phrasing differ.

  1. Sample a few thousand production prompts per route, within one scope (one tenant, one policy, one prompt version).
  2. For each prompt, find its nearest neighbor among the others in the same scope, and record the similarity score.
  3. Bucket the pairs by score into bands, for example 0.80–0.85 up to 0.98–1.00.
  4. Sample pairs from every band, weighting toward the bands you might choose as a threshold.
  5. For each pair, get the real provider answer for the second prompt and ask: would the first prompt's answer have been acceptable here? Label with humans, or with an LLM judge you have calibrated against humans first.1 See calibrating graders.
  6. Compute the false-hit rate per band with a confidence interval.

Report the upper bound. A band with zero observed bad hits in 60 pairs does not have a zero false-hit rate; with 95% confidence it could be near 5%.

false_hit.py
from math import sqrt

def wilson_upper(bad, n, z=1.96):
    if n == 0:
        return 1.0
    p = bad / n
    centre = p + z * z / (2 * n)
    margin = z * sqrt(p * (1 - p) / n + z * z / (4 * n * n))
    return (centre + margin) / (1 + z * z / n)

# For each band: wilson_upper(bad_hits, labeled_pairs)

Two refinements matter in practice. First, thresholds differ by intent: short factual lookups tolerate a looser threshold than prompts containing numbers, dates, negations or named entities. A cheap guard is a pre-check that extracts numbers, dates and identifiers from both prompts and refuses a hit when they differ. Second, a change of embedding model changes every score, so every threshold must be re-measured after one.

Step 3: run in shadow mode before serving a single hit

The offline sample estimates the false-hit rate on historical traffic. Shadow mode confirms it on live traffic without exposing users to it.

In shadow mode the lookup runs and logs "would have hit, cached entry X, score s", but the request still goes to the provider. You then compare the provider's fresh answer with the cached one for a sampled share of would-be hits, again with a calibrated judge or human review. Record, per request: the scope key, score, whether a would-be hit occurred, the label, and the estimated provider cost of the call you were not going to avoid yet.

Per-request cost and latency come from your gateway's request records; INFRO records model, latency, token counts and estimated provider cost for each request,3 and prompt and completion logging is off by default with a retention window you choose,3 so decide deliberately which comparison data you store and for how long. Keep the labeled comparison set under the same retention rules as the rest of your prompt logs.

Exit criteria for shadow mode, written before it starts:

  • would-be hit rate stable across at least a full weekly cycle,
  • upper-bound false-hit rate under budget in every band you intend to serve,
  • zero cross-tenant would-be hits,
  • no partition whose hit rate is dominated by one stale entry.

Step 4: price the saving against prompt caching, not a cold call

Finance owners often compute the benefit as "hit rate × cost of the call". That overstates it if the workload already benefits from provider prompt caching. With prompt caching the provider still serves the request but bills the repeated prefix at its cached-input rate, and INFRO's docs give ballpark cached reads of roughly 0.1× the input rate for OpenAI, Anthropic and Google, and lower for DeepSeek, with writes costing more than ordinary input.4 Those rates are the provider's, change over time, and your invoice is the authority.

A semantic hit avoids the remaining cost: the uncached part of the input (the new user message), all output tokens, and the latency. On a long-prefix, short-answer workload most input tokens are already cheap, so the marginal saving is smaller than the headline. On a short-prompt, long-answer workload, output dominates and the cache matters more.

Embedding lookups are model calls too. If you embed through your gateway, INFRO's /v1/embeddings takes OpenAI's request shape on your own connection,3 so the lookup cost shows up in the same per-request records as everything else. For the FinOps view, the FinOps Foundation lists a working-group asset on managing AI token costs from direct model provider APIs,5 which is the right place to align on how to report cache-avoided spend against the actual invoice.

Step 5: operate it like a data store with a wrong-answer budget

After launch, three things drift: your prompts, your documents and your embedding model.

  • TTL and invalidation. Pick the TTL from how fast the underlying facts change, not from the longest the system allows. INFRO's exact cache has a 60-second floor and plan-dependent ceilings, from one hour to 30 days,1 and a semantic cache deserves a shorter TTL than an exact one because each entry can be reused for many different phrasings. Purge by partition when the system prompt, policy or source documents change.
  • Continuous sampling. Keep auditing a small share of served hits against fresh answers, and alert when the upper-bound false-hit rate crosses budget. This is the same loop as shadow mode, kept running.
  • Attribution. Tag hits by tenant and feature so a bad partition is findable. Show hit rate, false-hit rate and avoided cost per route side by side; a rising hit rate with a rising false-hit rate is a threshold set too low, not a success.

The INFRO exact-match cache is a safer first step for most teams: a hit there is by construction the same request, and the gateway records the cache status per request.1 See exact-match caching and the prompt caching break-even for those two layers. Add a semantic layer only on routes where the measured numbers in steps 2 to 4 justify it.

When to say no

Skip semantic caching, or exclude the route, when any of these hold:

  • answers depend on per-user state, entitlements or retrieved private documents you cannot put in the key,
  • a wrong answer is costly (money movement, medical, legal, account changes),
  • prompts are long and unique, so few neighbors exist and the hit rate will be low,
  • the workload is already dominated by cheap cached-prefix input and short outputs.

Good candidates are the opposite: high-volume, public-knowledge questions inside one tenant's partition, with a stable system prompt, where a slightly different phrasing really does deserve the same answer.

Questions readers ask

What false-hit rate is acceptable?
There is no universal number. Set it per route from the cost of a wrong answer: a store-hours FAQ can tolerate more than anything touching billing, legal or account data. Write the budget down before the test, and require the upper confidence bound, not the observed rate, to sit under it.
Can a semantic layer sit in front of an exact-match gateway cache?
They are independent layers. INFRO's response cache is exact-match and does no similarity matching, so a semantic layer would be your own code in front of it, with its own key scoping, TTL and purge. Anything it returns never reaches the gateway, so those requests do not appear in gateway usage records unless you log them yourself.

From the team that builds it

Where this shows up in INFRO

INFRO is a control plane in front of the provider accounts you already hold. The capabilities below are the parts of it this article touches, each with the documentation that defines its behaviour.

  • Response cache

    Available with a plan

    Opt-in, exact-match cache for chat completions. An identical request inside the time-to-live you set is answered from the cache and never reaches your provider, so your provider does not bill it. Every plan; the longest time-to-live depends on the plan. No semantic matching.

    Read the documentation for Response cache
  • Usage and cost per request

    Available with a plan

    Every request records its model, latency, token or unit counts, and estimated provider cost, queryable by project and key.

    Read the documentation for Usage and cost per request
  • Request traces and payload logs

    Available with a plan

    Per-request timing detail, with prompt and completion logging off by default and a retention window you choose.

    Read the documentation for Request traces and payload logs
  • Embeddings

    Available with a plan

    POST /v1/embeddings in OpenAI's shape on your own OpenAI, Azure OpenAI, Mistral or OpenAI-compatible connection: routing that fails over only to the same model, budgets checked before the call, and the estimated provider cost of each request. Inputs follow your payload-logging setting; vectors are never stored.

INFRO accounts are self-serve. Provider usage is billed to you by your providers; INFRO charges a software subscription and takes 0% of model spend. See plans.

Sources

Numbered in order of first citation. Each was opened and checked against the claim on the date shown.

  1. 1.Response cache — Features (opens in a new tab) — INFRO Docs. Accessed Nov 4, 2026.
  2. 2.OWASP Top 10 for LLM Applications 2025 (opens in a new tab) — OWASP Gen AI Security Project. Accessed Nov 4, 2026.
  3. 3.INFRO documentation overview (opens in a new tab) — INFRO Docs. Accessed Nov 4, 2026.
  4. 4.Prompt caching — Features (opens in a new tab) — INFRO Docs. Accessed Nov 4, 2026.
  5. 5.The FinOps Foundation (opens in a new tab) — FinOps Foundation. Accessed Nov 4, 2026.

About this article

Read on 2026-11-04: INFRO's docs overview, response cache and prompt caching pages, the OWASP GenAI LLM Top 10 index and the FinOps Foundation home page. OpenAI's prompt caching guide returned HTTP 403 and Google's Gemini caching page was not readable from the allowed hosts, so provider caching behavior is cited only as INFRO's docs describe it; no similarity-threshold benchmarks or published hit rates are quoted, and all numbers in examples are illustrative.

Last checked against its sources on . The next review is due by . No separate named reviewer is recorded for this article.

How articles are written, sourced and corrected is set out in the editorial standards.