Tutorial: Multi-Tenant Setup

On this page

This tutorial shows how to register tenants with explicit quotas, understand what happens for unregistered tenants, and use the opt-in shared-cache feature.

1. Explicit registration

Python · 21 lines
from zerda.cache import CacheManager, CacheConfig, TenantRegistration

manager = CacheManager(CacheConfig())

manager.register_tenant(TenantRegistration(
    tenant_id="acme-corp",
    display_name="Acme Corp",
    memory_quota_mb=1024,
    max_entries=50_000,
    request_quota_rpm=600,
    allow_shared_read=False,
    allow_shared_write=False,
))

manager.register_tenant(TenantRegistration(
    tenant_id="beta-inc",
    display_name="Beta Inc",
    memory_quota_mb=256,
    max_entries=5_000,
    request_quota_rpm=100,
))

Every CacheManager also has a built-in "default" tenant, registered automatically from CacheConfig.tenant's defaults — you don't need to register it yourself, and (per TenantManager.deregister) you cannot remove it.

2. What happens for tenants you never registered

Python
result = manager.get_sync("hello", tenant_id="never-seen-before")

TenantManager._get_state auto-registers "never-seen-before" the first time it's used, under TenantConfig's default quota values (not "acme-corp"'s or "beta-inc"'s) — see tenant_manager.md. This means typos in a tenant_id string silently create a new, quota-limited tenant rather than raising an error — worth guarding against at your application's boundary if tenant IDs come from user input.

3. Quota enforcement in practice

Python · 13 lines
from zerda.cache.tenant_manager import QuotaExceeded

# Exhaust beta-inc's 100 requests/minute quota in a tight loop:
for i in range(150):
    result = manager.get_sync(f"query {i}", tenant_id="beta-inc")
    # get_sync() does NOT raise QuotaExceeded itself — CacheManager.get() catches it
    # internally and returns a miss. To observe the quota being hit directly, call
    # the TenantManager yourself:

try:
    manager._tenant_mgr.check_and_charge_request("beta-inc")
except QuotaExceeded as e:
    print(e.reason)

Note that CacheManager.get()/put() swallow QuotaExceeded and return a plain miss (hit=False) or None respectively — see cache_manager.md. If your application needs to distinguish "quota exceeded" from "genuine cache miss" to show a different message to the caller, you'll need to call TenantManager methods directly (as above) or track get_metrics()["errors"]["quota_exceeded"] counts, since that distinction isn't exposed on CacheLookupResult itself.

4. Shared cache

Python · 17 lines
manager.register_tenant(TenantRegistration(
    tenant_id="acme-corp",
    allow_shared_read=True,
    allow_shared_write=True,
))

# acme-corp publishes a shared answer:
await manager.put(
    "What is your refund policy?",
    "Standard 30-day refund policy applies.",
    tenant_id="acme-corp",
    is_shared=True,
)

# beta-inc can read it ONLY if beta-inc itself has allow_shared_read=True:
manager.register_tenant(TenantRegistration(tenant_id="beta-inc", allow_shared_read=True))
result = await manager.get("What is your refund policy?", tenant_id="beta-inc")

Whether beta-inc's lookup actually reaches acme-corp's shared entry depends on it being found via the exact-match or semantic-search path — sharing does not create a tenant-independent lookup; it only relaxes the access check (TenantManager.is_accessible) once a candidate entry has otherwise been located.