What is semantic caching and when is it safe to use?
954 views
66 of 83
1
The problem
Question presented to candidate:
"Half the questions our support bot gets are the same twenty questions in different words. Someone proposed a semantic cache: if a new question is close enough to one we have answered, return the stored answer and skip the model. What is a semantic cache, how does it work, and when would it be dangerous to turn on?"
What a strong answer should cover:
Mechanism: embed the incoming query, nearest-neighbour search over embeddings of past queries, return the stored answer if similarity clears a threshold, otherwise call the model and store the result; layered after an exact-match cache
How it differs from provider prompt caching: prompt caching reuses the processed prompt prefix and still generates a fresh answer; a semantic cache skips the model and returns an old answer, so it can be wrong
The core risk: embeddings measure topic, not intent or exact values, so "cancel my order" versus "can I cancel an order?" or "convert 100 USD" versus "convert 250 USD" can score as near-identical and get the wrong answer; no single threshold separates every bad pair from every good one
Safety rails: scope keys (tenant, user or personalisation, prompt version, model, locale), TTLs tied to how fast the content changes, bypass for transactional, personalised or tool-using requests, entity or value checks, and invalidation when source documents change
Choosing the threshold from labelled pairs (served, wrong, missed) and monitoring the wrong-hit rate in production, not only the hit rate
When it fits: high-volume, repetitive, informational, non-personalised traffic; when it does not: anything with account state, actions, numbers, fast-changing facts or per-user context
Clarifying questions expected:
Are answers the same for every user, or do they depend on account, plan, locale or permissions?
How often does the underlying content (policies, prices, docs) change?
What does a wrong cached answer cost compared with a model call?
Code / implementation expected: Optional โ the lookup flow (embed, search within scope, threshold, guards, store) in pseudocode, and how the threshold is chosen, is enough.
semantic-caching
cost
caching
risk
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Engineers with repetitive LLM traffic (support bots, FAQ assistants, internal help desks) who have been told a semantic cache will halve the bill, and want to know when that is true and when it serves wrong answers.
Difficulty: Hard
Verified with: Node 22.22.0 and Python 3.12.3 (a semantic cache with safety rails, and a threshold sweep on labelled pairs). The embedding in both examples is a toy stand-in (words mapped to concepts) so the similarity numbers are explainable; real embedding models give different numbers but fail in the same ways. Library and paper details are cited inline, checked October 2026.
How to read this doc: Every idea is explained in plain language first, then tagged with ๐ Interview term: so you also learn the words an interviewer expects. All program output shown was produced by actually running the examples; the **Verifi
// Pick the threshold from labelled data, not from a blog post.// Each pair: a new query, the cached query it is nearest to, and whether the// cached answer is actually correct for the new query (labelled by a human).constLEX = {
password: ["password", 1], reset: ["reset", 1], forgot: ["reset", 1], steps: ["procedure", 1],
how: ["procedure", 0.3], can: ["ability", 0.3], cancel: ["cancel", 1], order: ["order", 1],
refund: ["refund", 1], policy: ["policy", 1], convert: ["convert", 1], usd: ["usd", 1],
eur: ["eur", 1], store: ["store", 1], open: ["open", 1], when: ["time", 0.3],
};
constSTOP = newSet(["do", "i", "my", "to", "is", "your", "the", "a", "an", "what", "does", "on", "for"]);
constwords = (q) => q.toLowerCase().replace(/[?.!,]/g, "").split(" ").filter(Boolean);
functionembed(q) {
const v = {};
for (const w ofwords(q)) {
if (STOP.has(w)) continue;
const [dim, wt] = /^\d+$/.test(w) ? ["NUM", 1] : LEX[w] || ["w:" + w, 1];
v[dim] = (v[dim] || 0) + wt;
}
return v;
}
functioncosine(a, b) {
let dot = 0, na = 0, nb = 0;
for (const k ofObject.keys(a).sort()) { na += a[k] * a[k]; if (k in b) dot += a[k] * b[k]; }
for (const k ofObject.keys(b).sort()) nb += b[k] * b[k];
return dot / Math.sqrt(na * nb);
}
constnumbers = (q) => words(q).filter((w) =>/^\d+$/.test(w)).join(",");
constACTIONS = newSet(["cancel", "delete", "change"]);
constisTransactional = (q) => { const ws = words(q); return ws.some((w) =>ACTIONS.has(w)) && (ws.includes("my") || ACTIONS.has(ws[0])); };
const pairs = [
["password reset steps", "How do I reset my password?", true],
["forgot password how to reset", "How do I reset my password?", true],
["reset my password", "How do I reset my password?", true],
["refund policy", "What is your refund policy?", true],
["Convert 100 USD to EUR please", "Convert 100 USD to EUR", true],
["can I cancel my order", "Can I cancel an order?", true],
["what time does the store open", "When does the store open?", true],
["cancel my order", "Can I cancel an order?", false],
["Convert 250 USD to EUR", "Convert 100 USD to EUR", false],
["When does the store open on Sunday?", "When does the store open?", false],
["refund policy for electronics", "What is your refund policy?", false],
["delete my account", "How do I reset my password?", false],
];
console.log("similarity per labelled pair:");
for (const [q, c, ok] of pairs) console.log(` ${cosine(embed(q), embed(c)).toFixed(3)}${ok ? "same " : "DIFF "} "${q}"`);
console.log("");
console.log("threshold mode served correct WRONG missed-good");
for (const th of [0.8, 0.85, 0.9, 0.95, 0.99]) {
for (const guarded of [false, true]) {
let served = 0, correct = 0, wrong = 0, missed = 0;
for (const [q, c, ok] of pairs) {
let hit = cosine(embed(q), embed(c)) >= th;
if (guarded && (isTransactional(q) || numbers(q) !== numbers(c))) hit = false;
if (hit) { served++; if (ok) correct++; else wrong++; } elseif (ok) missed++;
}
console.log(` ${th.toFixed(2)}${(guarded ? "guarded" : "naive").padEnd(8)}${String(served).padStart(4)}${String(correct).padStart(6)}${String(wrong).padStart(5)}${String(missed).padStart(6)}`);
}
}
5
Discussion
Other approaches, follow-ups and how it went in real interviews.