Skip to main content
When several tenants, users, or environments share one LMCache deployment, you usually want two kinds of isolation:
  • Reuse isolation — control who is allowed to share cached prefixes. A prompt cached for one tenant must not be reused by another. This is what cache_salt provides.
  • Capacity isolation — control how much cache each tenant may consume, so one noisy tenant cannot evict everyone else’s entries. This is what per-tenant quotas and the IsolatedLRU eviction policy provide.
This page covers both, and which layer owns each setting.

Who configures what

Multi-tenancy spans three layers. Putting a setting in the wrong layer is the most common mistake.
ConcernOwnerWhere it is set
Reuse boundary per request (cache_salt)API caller (vLLM / your app)The request body sent to vLLM — not an operator field
Engine deployment, cache size, eviction configOperator (TMO)LMCacheEngine CRD / Helm values
Per-tenant storage budgets (quotas)Runtime adminLMCache HTTP cache-admin API on the engine (or coordinator)
Fleet-wide per-tenant usage trackingOperator (TMO)LMCacheEngine.spec.coordinator + an LMCacheCoordinator
Note in particular: the operator deploys the LMCache engine, but it does not set cache_salt. There is no cache_salt field in any CRD or Helm value. The salt is attached to each inference request by the caller.

Reuse isolation with cache_salt

cache_salt is a per-request isolation key. It tells LMCache which requests are allowed to share cached KV and which ones must stay separate. The behavior is simple:
  • same model + same prompt prefix + same cache_salt → cache reuse is allowed
  • same model + same prompt prefix + different cache_salt → cache reuse is blocked
  • no cache_salt → the request uses the default unsalted cache domain
Internally, cache_salt is part of the cache key identity (the ObjectKey), alongside the chunk hash, model name, and worker rank. Two requests whose content hashes identically but whose salts differ resolve to distinct keys, so they never collide in L1 or L2. Unsalted traffic uses the same on-disk key format as before salts existed, so introducing a salt for some tenants does not invalidate existing unsalted entries.

How to set it

cache_salt is a first-class field on OpenAI-compatible requests to vLLM. Include it directly in the request body.
curl -s http://localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "Qwen/Qwen3-0.6B",
    "messages": [
      {"role": "user", "content": "Summarize the quarterly report."}
    ],
    "cache_salt": "tenant-acme",
    "max_tokens": 128,
    "temperature": 0
  }'
For plain completions:
curl -s http://localhost:8000/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "Qwen/Qwen3-0.6B",
    "prompt": "Summarize the quarterly report.",
    "cache_salt": "tenant-acme",
    "max_tokens": 128,
    "temperature": 0
  }'
vLLM propagates the salt through its LMCache connector to both the scheduler (lookup) and worker (store/retrieve) paths, so it consistently scopes reads and writes for that request.

How to choose a value

Pick a value that is:
  • stable across requests that should share cache
  • different across requests that must stay isolated
  • low-cardinality enough that it does not explode your metrics labels
Good choices: tenant-acme, customer-123, prod, workspace-42. Bad choices: a random UUID per request, a timestamp, or any value that changes on every call. If the value changes on every request, you destroy reuse and effectively turn every request into a cache miss.

Constraints

cache_salt is validated by LMCache. It:
  • must not contain @
  • must not contain /
  • must not contain \
  • must not contain a NUL byte
  • must be at most 128 characters
A salt that violates these is rejected with a ValueError. Keep it simple: short ASCII identifiers such as tenant-acme or prod are the safest choice.
The empty string ("") is the default, unsalted domain. In the cache-admin HTTP API described below, the empty salt cannot appear in a URL path, so it is represented by the reserved sentinel _default (for example GET /quota/_default).

Capacity isolation: per-tenant quotas

Reuse isolation keeps tenants from reading each other’s data. It does not stop one tenant from filling the cache. For that, LMCache supports per-tenant quotas backed by the IsolatedLRU eviction policy. How it works:
  • IsolatedLRU keeps a separate LRU ordering per cache_salt instead of one global list, so eviction victims for one tenant are chosen only from that tenant’s entries.
  • A quota assigns each cache_salt a storage budget in bytes. LMCache tracks usage per salt and evicts a salt’s oldest entries once it crosses its budget.
  • Quotas use allowlist semantics: a cache_salt with no registered quota has an effective limit of 0 bytes. Its stores are accepted on the hot path but evicted on the next eviction cycle (~1s). Only salts with an explicit quota retain cached data. Plan to register a quota for every tenant you want cached.
The eviction loop is governed by these engine-side fields (LMCache eviction config):
FieldDefaultMeaning
eviction_policyLRULRU, IsolatedLRU, or noop
trigger_watermark0.8Per-salt usage fraction (of quota) that triggers eviction
eviction_ratio0.2Fraction of over-quota bytes evicted per cycle
Operator support is currently partial. The LMCacheEngine CRD’s structured eviction.policy field only accepts LRU today (eviction.triggerWatermark and eviction.evictionRatio are exposed). To run IsolatedLRU, pass it to the engine through the CRD escape hatches — spec.extraArgs (extra CLI flags appended last) or spec.env — rather than eviction.policy. Per-tenant quotas themselves are always set at runtime through the HTTP API below, not through the CRD.

Managing quotas at runtime

The engine exposes an HTTP cache-admin frontend on spec.httpPort (default 8080). Quotas are created, inspected, and removed there:
MethodPathBodyPurpose
PUT/quota/{cache_salt}{"limit_gb": <float>}Create or update a tenant’s budget
GET/quota/{cache_salt}Read a tenant’s limit and current usage
DELETE/quota/{cache_salt}Remove a budget (entries evicted next cycle)
GET/quotaList every registered quota and its usage
# Give tenant-acme a 5 GB budget (engine HTTP frontend on httpPort 8080)
curl -s -X PUT http://<engine-host>:8080/quota/tenant-acme \
  -H 'Content-Type: application/json' \
  -d '{"limit_gb": 5}'

# Inspect it
curl -s http://<engine-host>:8080/quota/tenant-acme
Use the _default sentinel to manage the unsalted domain: PUT /quota/_default.

Fleet-wide quotas via the coordinator

The HTTP API above is per-engine. To track and bound a tenant’s usage across an entire fleet of engines, register the engines with an LMCacheCoordinator and enable event reporting on the engine’s coordinator block:
apiVersion: lmcache.ai/v1alpha1
kind: LMCacheEngine
spec:
  coordinator:
    ref:
      name: my-coordinator      # an LMCacheCoordinator in the same namespace
    l2EventReporting: true      # report L2 store/lookup events for fleet-wide tracking
    l2EventFlushInterval: 1     # seconds between event flushes
With reporting on, each engine forwards per-salt usage events to the coordinator, which aggregates them and runs fleet-wide per-salt quota enforcement and eviction. The coordinator side exposes the matching admin endpoints (PUT/DELETE /l2/quota/{cache_salt}, GET /l2/status[/{cache_salt}]) and its own sweep controls (evictionCheckInterval, default 5s; evictionRatio, default 0.2).

Deployment-level isolation

cache_salt and quotas isolate tenants within one shared cache. For harder isolation, give each tenant its own engine:
  • Per-namespace engines — run a separate LMCacheEngine CR per tenant namespace. The chart’s multi-tenant mode (operator.enabled=true, engine.enabled=false) installs only the operator; each tenant then ships their own engine CR. Caches are not shared across separate engines at all.
  • Node placementspec.nodeSelector, spec.affinity, and spec.tolerations pin a tenant’s engine to dedicated GPU nodes.
  • RBAC / networkspec.serviceAccountName, spec.podLabels, and spec.podAnnotations let you attach per-tenant RBAC and NetworkPolicy.
Choose deployment-level isolation when tenants must not share GPUs or memory at all; choose cache_salt + quotas when you want the efficiency of one shared pool with logical separation.

Observability

LMCache tags several metrics with cache_salt, which is useful for per-tenant hit-rate analysis:
  • lmcache_mp.lookup_requested — tokens submitted for lookup (labels: model_name, cache_salt)
  • lmcache_mp.lookup_hit — tokens served from cache (labels: model_name, cache_salt)
  • lmcache_mp.real_reuse_gap — seconds between a chunk’s last write and next read (label: cache_salt)
  • lmcache_mp.real_reuse_gap_objects — the same gap measured in chunks (label: cache_salt)
Divide lookup_hit by lookup_requested, grouped by cache_salt, for a per-tenant hit rate.
Treat cache_salt as a potentially high-cardinality dimension. Per-tenant or per-environment salts are fine; per-request salts will blow up your metrics cardinality (and destroy reuse).

Verification

cache_salt reuse behavior is straightforward to check by hand:
  1. Send a long prompt with cache_salt: "tenant-a".
  2. Send the same prompt again with cache_salt: "tenant-a" → reuse is allowed.
  3. Send the same prompt with cache_salt: "tenant-b" → it must not reuse tenant-a’s entries, even though the prompt is identical.
To confirm a hit at the engine level, inspect the engine pod logs — the engine logs KV cache registration and serving activity that the end-to-end test harness keys on to assert cache behavior. Per-tenant hit rates are also visible through the metrics above.
The current end-to-end test suite exercises basic cache hit/miss and L1/L2 storage, but does not yet include automated coverage for cache_salt isolation, per-tenant quotas, or IsolatedLRU. Validate these flows in a staging deployment before relying on them in production.
  • Use no cache_salt if all traffic is inside one trust boundary.
  • Use one stable salt per tenant or environment when traffic must be isolated.
  • Avoid per-request salts unless you explicitly want to disable sharing.
  • If you need capacity fairness, enable IsolatedLRU and register a quota for every tenant you want cached (remember: unregistered salts get 0 bytes).
  • For strict isolation, give each tenant its own engine per namespace instead of a shared pool.