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.

Related pages