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
flatordynamicindex 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
dynamicorflatindexes for small tenants.