FAQ
On this page
Does CacheManager call an LLM for me?
No. Nothing in cache/cache_manager imports an LLM client or makes an outbound API call
to a language model. CacheManager.get() only tells you whether to call an LLM
(result.hit == False); invoking the LLM and passing its answer to put() is entirely the
caller's responsibility, as shown throughout the Quickstart.
Which embedding provider should I use?
EmbeddingProvider.MOCK (the default) works out of the box with no external dependency and
is suitable for development and testing, but produces vectors with no real semantic
relationship to the input text — it will never produce genuine semantic-similarity hits.
The other three providers (OPENAI, HUGGINGFACE, OLLAMA) all depend on an external
zerda.embeddings module not included in this codebase; see
Known Issues
before choosing one of them for production.
Can I plug in my own storage backend?
Yes, by subclassing BaseStorage and implementing all nine
abstract methods. There's no public CacheManager constructor parameter to inject a custom
storage instance directly, so you'd need to replace manager._storage after construction
(an accepted, if slightly unusual, pattern given BaseStorage's clean interface — see the
extending example).
Is MultiLevelCache safe for caching large objects?
It works, but be aware that: (1) every entry's size is estimated by pickling it once (see
CacheEntry), and (2) L3 storage also
pickles values to disk, which is not designed for very large objects or extremely
high-throughput workloads. For large or numerous objects, consider the semantic-cache
pipeline's pluggable storage backends instead, or a
dedicated object store.
How does multi-tenancy actually isolate data?
Two independent mechanisms both implement the same "owner or explicitly shared" rule:
TenantManager.is_accessible (which is
the one CacheManager.get() actually calls) and
SecurityGuard.enforce_tenant_access (which exists as a
public method but isn't called from the default pipeline). Cache keys are also
tenant-scoped at the hashing level (QueryNormalizer.exact_cache_key incorporates
tenant_id), so two tenants asking the identical question never share an exact-match
cache entry unless one of them opts into is_shared=True on put() and the reading
tenant has allow_shared_read=True.
Why does the RL/bandit system exist — what does "feedback" actually change?
Calling CacheManager.feedback()
updates a per-entry Beta-distribution bandit arm
(BanditArm) that influences two later decisions: (1) how
candidates are ranked during semantic search
(CachePolicyLearner.rank_candidates),
and (2) which entries get evicted first during the background eviction cycle
(CachePolicyLearner.eviction_ranking).
It also nudges the adaptive similarity threshold. Without any feedback calls, every entry's
bandit arm stays at its prior (alpha=1.0, beta=1.0, i.e. expected_reward=0.5) forever.
What happens if Redis is configured but unreachable?
RedisStorage silently falls back to a local
SQLiteStorage at construction time, logging a warning but not
raising. This fallback is permanent for the instance's lifetime — it will not reconnect to
Redis later even if it becomes reachable. Check your logs (or add a health check) if you
need to confirm Redis is actually in use.
Does the library run any background threads I should know about?
Yes: CacheManager starts a daemon threading.Timer for its eviction cycle
(config.eviction.check_interval_s, default 300s), and MultiLevelCache starts a similar
timer for expired-entry cleanup (config.auto_cleanup_interval, default 3600s). Both are
daemon=True, so they won't prevent process exit, but calling close() (or using the
context-manager form) explicitly cancels them.
Where can I find a list of things that don't currently work as documented?
See Known Issues & Caveats — it covers everything from a blocking
AttributeError in the embedding pipeline to smaller inconsistencies in TTL handling and
metrics counting.