Home / AI & GenAI / Vector Databases

AI & GenAIHow-toIntermediate

Weaviate multi-tenancy in production: tenants per customer, activity status, offloading and the errors you'll meet

Weaviate's native multi-tenancy gives every customer its own shard inside one collection. Set it up, create and manage tenants, deactivate or offload idle tenants to save memory, and fix the common "tenant not found" and "tenant not active" errors.

Deepak Bhardwaj4 min read

Applies toWeaviate 1.25+ (ACTIVE/INACTIVE/OFFLOADED statuses; offloading needs the offload module), Python client v4; Weaviate Cloud and self-hosted

Why native multi-tenancy

With a tenant_id property and a filter, every tenant's data sits in the same index; each query searches through the shared graph and filters. Weaviate's multi-tenancy instead creates one shard per tenant inside a collection:

Benefit How
Isolation A query names a tenant and only touches that tenant's shard
Fast deletes Deleting a tenant drops its shard; no tombstones in a shared index
Memory control Idle tenants can be deactivated (unloaded) or offloaded to object storage
Scale Hundreds of thousands of tenants per cluster are supported, spread over nodes

Step 1: create a multi-tenant collection

import weaviate
from weaviate.classes.config import Configure, Property, DataType
from weaviate.classes.tenants import Tenant, TenantActivityStatus

client = weaviate.connect_to_custom(...)   # or connect_to_weaviate_cloud(...)

client.collections.create(
    "KbChunk",
    multi_tenancy_config=Configure.multi_tenancy(
        enabled=True,
        auto_tenant_creation=False,     # True creates tenants on first insert - convenient, but typos become tenants
        auto_tenant_activation=True,    # an INACTIVE tenant is activated when used
    ),
    properties=[
        Property(name="doc_id", data_type=DataType.TEXT),
        Property(name="content", data_type=DataType.TEXT),
    ],
)

Multi-tenancy must be enabled at creation; you can't switch an existing collection to it. Moving an existing single-tenant collection means creating a new one and re-importing.

Step 2: manage tenants

kb = client.collections.get("KbChunk")

kb.tenants.create([Tenant(name="t-acme"), Tenant(name="t-globex")])
print(kb.tenants.get())                       # dict of name -> Tenant(status=...)
kb.tenants.remove(["t-globex"])               # deletes the tenant and ALL its data

Tenant names: use stable internal IDs, not display names.

Step 3: read and write through the tenant

acme = kb.with_tenant("t-acme")

with acme.batch.fixed_size(batch_size=200) as batch:
    for chunk in chunks:
        batch.add_object(properties={"doc_id": chunk.doc_id, "content": chunk.text}, vector=chunk.embedding)

res = acme.query.near_vector(near_vector=qvec, limit=5)

Any operation on a multi-tenant collection without a tenant fails. That's a useful safety property: a code path that forgets the tenant fails loudly instead of leaking data.

Step 4: activity status

Status Data Memory Query
ACTIVE On local disk, loaded Uses memory Yes
INACTIVE On local disk, not loaded Minimal No (unless auto-activation)
OFFLOADED Moved to cloud object storage None locally No; must be reactivated (onloaded) first

Older versions used HOT, COLD and FROZEN for these states.

kb.tenants.update([Tenant(name="t-acme", activity_status=TenantActivityStatus.INACTIVE)])
kb.tenants.update([Tenant(name="t-acme", activity_status=TenantActivityStatus.ACTIVE)])

Offloading needs an offload module (for example offload-s3) configured on the cluster. Onloading a large tenant takes time; don't offload tenants that might be queried interactively without a "waking up" message in the UI.

Step 5: an idle-tenant policy

from datetime import datetime, timedelta

def deactivate_idle(kb, last_used: dict, idle_days=14):
    cutoff = datetime.utcnow() - timedelta(days=idle_days)
    idle = [Tenant(name=t, activity_status=TenantActivityStatus.INACTIVE)
            for t, ts in last_used.items() if ts < cutoff]
    for i in range(0, len(idle), 100):
        kb.tenants.update(idle[i:i + 100])

Track last_used per tenant in your application (Weaviate doesn't report usage per tenant directly). With auto_tenant_activation=True, the next query activates the tenant automatically, at the cost of a slower first query.

Step 6: common errors

Error Cause Fix
has multi-tenancy enabled, but request was without tenant Code path without with_tenant Route all access through a tenant-aware helper
tenant not found Tenant never created, or typo Create tenants in onboarding; avoid auto_tenant_creation unless you validate names
tenant not active Tenant INACTIVE/OFFLOADED and auto-activation off Activate it, or enable auto-activation
Slow first query for a tenant Activation/loading Expected; warm tenants before known usage
Cross-references fail References between multi-tenant and non-MT collections have rules Check reference rules in the docs for your version

Step 7: offboarding

kb.tenants.remove(["t-acme"])
assert "t-acme" not in kb.tenants.get()

That deletes all of the tenant's objects in that collection. Do it for every multi-tenant collection the tenant has, and remember backups taken before deletion still contain the data until they expire.

Sizing notes

  • Each active tenant has its own HNSW index (or flat/dynamic index). Many tiny tenants with HNSW waste memory; for small tenants the flat or dynamic index type (flat that switches to HNSW above a threshold) is usually a better fit.
vector_index_config=Configure.VectorIndex.dynamic(threshold=10_000)

(requires async indexing to be enabled on the server; check your version's docs).

  • Spread tenants across nodes: in a multi-node cluster, tenant shards are distributed automatically.

Prevent problems next time

  • One tenant-aware data-access helper; no direct collection access in application code.
  • Create tenants in your onboarding flow, not implicitly.
  • Deactivate idle tenants on a schedule and monitor activation latency.
  • Use dynamic or flat indexes for small tenants.