Pointsman can reuse an exact request or an eligible semantically similar request. This guide explains what permits a hit, what rejects one and which controls an operator can inspect. See the cache architecture for the request flow and cache tuning for configuration steps.
Two tiers, two questions#
- L1 is exact: an in-process cache answering "has this tenant sent the same
content and answer contract before?" Its configured entry-equivalent and payload-weight
bounds limit retention; the default TTL is 15 minutes. Estimated payload weight is not
a measurement of JVM heap. See configuration. It is
consulted first, and its hits read
X-Cache: HIT-L1. - L2 is semantic: a Redis vector index answering "has this tenant asked this question in
other words?" The prompt is embedded (MiniLM, locally, 384 dimensions), the index searched,
and a stored entry whose cosine similarity clears the threshold (
0.92by default, disclosed on every response asX-Cache-Threshold) is a candidate hit:HIT-L2.
L2 hits avoid provider tokens, but approximate reuse can return an unsuitable answer. An approximate index can also omit an existing exact vector; a shared store does not guarantee recall.
A successful L2 replay can warm the serving replica's L1 only when its stored exact-cache
fingerprint matches the current request. The controller first checks the current caller's
allowed model/output cap and applies output guardrails; it then stores the guarded response
in L1. Paraphrases and older entries without that fingerprint are not promoted. The first
response still reports HIT-L2; subsequent identical requests can report HIT-L1, under
L1's normal TTL, weight and invalidation policy. This avoids repeated approximate searches
for already-validated exact requests; it does not guarantee a hit on a cold replica.
The guards against serving the wrong answer#
A semantic candidate must pass each applicable check before reuse.
| Check | Requirement | Tradeoff or observation |
|---|---|---|
| Similarity and lexical verification | Clear the similarity threshold and word-overlap floor (verification-min-overlap, default 0.4) |
Sampled hits and scores are recorded in cache_hit_samples for evaluation |
| Tenant scope | Exact keys and semantic namespaces/query filters bind tenant identity | Reuse cannot cross the admitted tenant scope |
| Redacted content | Derive keys after input guardrail redaction | Requests with different effective content do not share the same exact fingerprint |
| Conversation history | Default context mode includes the prior conversation exactly | Lower multi-turn hit rates protect against reusing another conversation's answer |
| Minimum text length | Prompts under 20 characters skip L2 | skipped-short; exact L1 can still serve |
| Sampling policy | Default semantic reuse requires explicit temperature zero | Nonzero or omitted temperature stays off L2 under exact-only; L1 can still reuse identical requests |
| Tools | Requests carrying tools skip semantic reuse | Exact L1 reuse remains subject to the request contract |
| Serving contract | Check model permissions, output cap, generation and response policy | A vector match alone is insufficient |
Alternative context modes loosen exact-history matching. Review each mode's tradeoff before using X-Cache-Context-Mode. X-Cache-Temperature-Mode changes the documented read/semantic policy; it is not a substitute for validating the workload.
Streaming, and the failure posture#
Cached responses can be replayed as SSE. The response must already be complete and pass the normal serving checks.
| Streaming control | Behavior |
|---|---|
| Default replay | One immediate content delta |
Optional paced replay |
Word-by-word replay with an 8 ms inter-chunk delay |
pointsman.cache.streaming.enabled=false |
Disable streaming cache lookup and write-through; assembly for ledger, counters and reward remains |
Cache failures normally become a miss or a skipped write. Redis commands default to a 50 ms timeout, and the L2 breaker skips work while open. That timeout is per command, not a bound on embedding plus the entire multi-command lookup. A cache miss still passes provider admission and budget checks.
Eligible concurrent identical misses can share one provider computation. A follower that exceeds its wait bound proceeds independently; replay still requires the follower's own serving checks.
Operating it#
Use request directives, invalidation and capacity controls for different purposes.
| Control | Scope | Behavior |
|---|---|---|
X-Cache-Control: no-cache |
One request | Skip reads on both tiers; allow eligible writes |
X-Cache-Control: no-store |
One request | Skip reads, writes and single-flight |
DELETE /internal/cache?tenant=... |
One tenant | Invalidate literal tenant ID; glob characters are refused |
pointsman.cache.version |
Global key namespace | Changing the salt makes prior keys unreachable |
| TTL | Retention | L2 defaults to 24 hours, extends on hits, and has a 168-hour hard cap; volatile models may use shorter TTLs |
For tenant invalidation, the receiving instance flushes shared L2 and publishes on illg:cache:invalidate; subscribers drop local L1. This describes invalidation behavior and does not expand the qualified deployment scope.
Bound L2 growth#
Unique prompts can grow the index throughout their retention period. Configure both document admission and Redis memory limits.
- Set
POINTSMAN_CACHE_L2_MAX_DOCUMENTS(default 200,000). New writes stop at the observed threshold and resume as entries expire or are evicted. - Set
POINTSMAN_CACHE_L2_SIZE_CHECK_MS. Queued writes recheck admission; stale checks close writes. - Give cache-only Redis a
maxmemorybound and anallkeys-lruorallkeys-lfupolicy. - Use
POINTSMAN_CACHE_L2_STORE_CHECKS=enforceto stop writes when required store protections are missing. Defaultreportexposes the condition without enforcing it.
The document threshold is soft: already-admitted writes, including writes from other replicas, can still land. The memory limit is the hard bound. A skipped cache write is observable and does not refuse a completed client response.
Counter Redis must use
noeviction. Do not apply the cache eviction policy to a store that holds budget counters.
Inspect gateway.cache.store.skipped, gateway.cache.l2.store_checks_failing and the l2WriteAdmission health detail. Hit rates, hit/miss latency and estimated savings are available at GET /admin/observe/analytics/cache; response verdicts use the cache headers.
Hosting your own embedding model#
MiniLM is the default because it runs in-process with no key and no network. An operator with a
larger embedding model - self-hosted, or a provider's - can put it behind the cache instead, as
long as it speaks the OpenAI-compatible /v1/embeddings shape:
POINTSMAN_CACHE_EMBEDDER=hosted
POINTSMAN_CACHE_HOSTED_BASE_URL=https://embeddings.internal # required
POINTSMAN_CACHE_HOSTED_MODEL=my-embedding-model # required
POINTSMAN_CACHE_HOSTED_MAX_INPUT_TOKENS=8191 # required, see below
POINTSMAN_CACHE_HOSTED_API_KEY_ENV=EMBEDDINGS_API_KEY # optional: the VARIABLE's name
The vector dimension is not configured: the boot probe embeds one string and reads the width
off the answer. If the probe fails, the gateway still starts, the semantic tier misses (the
gateway.cache.embedder.semantic gauge reads 0) and the embedder re-probes every 30 seconds
until the endpoint answers.
max-input-tokens is mandatory, and the gateway refuses to start without it. No embeddings
API discloses its context window, so there is no value the gateway could safely assume. What you
declare here becomes the cache's skip threshold: a prompt estimated to be longer than the window
is not embedded and not cached, and the response says so (X-Cache-Semantic: skipped-window).
This is
the setting to raise when a large model should be caching long prompts that MiniLM never could.
Hosted-window estimation#
The guard is text.length() / 4 <= maxInputTokens - four
characters per token, the same divisor the rest of the gateway uses for character-based token
estimates. Dense code or non-Latin text tokenises heavier than that, so a prompt the guard admits
can still exceed the provider's real limit by a margin. Declare the real window, not a rounder
number above it.
The declared window must match the embedding service's real capability.
| Declaration | Consequence |
|---|---|
| Smaller than the real window | Conservative, visible skips for longer prompts |
| Larger than the real window | Provider rejection or silent truncation; a fragment vector can be stored under a full-request fingerprint |
The gateway fingerprint cannot detect a provider silently truncating an input without changing its declared identity. Verify the service's behavior with boundary-length inputs.
Changing the embedding window#
The declared window is part of the embedder fingerprint
(text:hosted-<model>:<dim>:w<window>), alongside the model name and the probed dimension. Change
any of the three and the semantic tier starts cold: old entries are unreachable under the new
scope and age out by their TTL. That is deliberate - vectors computed under one window are not
comparable with vectors computed under another - and it means a window change costs one cache
warm-up, not one silent mismatch.
Supported embedding scope#
The tiers and key namespaces are modality-agnostic - the design carries seams for image,
audio, and document embedders - but only the text embedder ships. If the MiniLM model
cannot load, the embedder falls back to lexical hashing, loudly; deployments where a false
positive is worse than an outage can set POINTSMAN_CACHE_REQUIRE_SEMANTIC=true and refuse
to start instead.
Lexical verification safety boundaries#
The default lexical verifier requires stored source text. Legacy entries without it are misses and are replaced by normal write-through; switching verification on no longer silently accepts unverifiable old answers. Word matching uses Unicode letters and numbers, so different non-Latin prompts do not compare as two empty ASCII sets.
Digit-bearing, whitespace-delimited tokens must match literally in order, including
signs, punctuation, identifiers and repetitions. Changing 100 to 1000, AB123
to AB124, or 1/2 to 2/1 cannot pass just because most words match. This is a
conservative filter: a valid paraphrase that changes punctuation around a number
can miss. verification=none explicitly disables these checks along with lexical
overlap. No new numerical threshold is introduced.
This remains a heuristic, not proof that two questions have interchangeable answers.
It does not establish equivalence of spelled-out numbers, negation, units or other
facts. For workloads requiring literal request matching, disable semantic text reuse
with pointsman.cache.modalities.text.enabled: false; the L1 exact cache remains
available. Use X-Cache-Control: no-store to bypass both tiers for a request. The
shipped configuration enables semantic text reuse for eligible requests, including
temperature: 0. Despite its name, temperature-mode: exact-only skips semantic
reuse only for nonzero or omitted temperature; it is not a global L2-off switch.
Evaluate semantic reuse against a labeled workload before relying on it.
The hashing embedder can map distinct texts to identical vectors; unique request text
alone is not a guarantee of a cache miss in a semantic-cache benchmark.