skip to content

How do you decide between Weaviate tenants, one collection per customer, and a tenant property?

level: principalimportance: should knowfreq 55%

answer

  1. Isolation the engine enforces, not the query author
  2. One shard and index per tenant
  3. Offboarding as a metadata drop
  4. Idle customers can stop costing memory
  5. The flag cannot be turned on later

basics

~20 s

Use built-in multi-tenancy for many isolated customers: each tenant gets its own shard and index, cheap deletion, and an activity status that offloads idle tenants. A collection per customer does not scale in schema metadata; a tenant filter shares one index and gives no isolation.

solid answer

~50 s

Enable multi-tenancy at creation with `multi_tenancy_config=Configure.multi_tenancy(enabled=True, auto_tenant_creation=True)`, then address data through `collection.with_tenant("acme")`. Each tenant is its own shard with its own vector index, which buys three things: real isolation, deletion of a customer as a metadata operation via `tenants.remove` rather than a mass delete, and a lifecycle — tenants can be active, inactive or offloaded, so idle customers stop consuming memory. A collection per customer looks similar but pushes every customer into the cluster's schema and coordination state, which does not scale to thousands and makes cross-cutting schema changes an N-way migration. A `tenant_id` property with a filter shares one index across all customers: neighbours affect each other's latency, deleting a customer is a slow bulk delete, and isolation is only as good as every query remembering the filter. The flag is immutable, so decide before you load data.

code

python · 17 lines
python
import weaviate
import weaviate.classes.config as wvcc
from weaviate.classes.tenants import Tenant

client = weaviate.connect_to_local()
client.collections.create(
    name="CustomerDoc",
    multi_tenancy_config=wvcc.Configure.multi_tenancy(
        enabled=True,
        auto_tenant_creation=True,
    ),
)

docs = client.collections.get("CustomerDoc")
docs.tenants.create([Tenant(name="acme"), Tenant(name="globex")])
docs.with_tenant("acme").data.insert({"body": "hello"})
client.close()

go deeper

for a junior

Know that Weaviate has built-in multi-tenancy, that it is switched on in the collection definition, and that data is then read and written through a tenant-scoped handle.

for a middle

Explain what a tenant buys mechanically — its own shard and vector index, tenant-scoped calls that error without a tenant, removal as a metadata operation — versus filtering on a tenant property.

for a senior

Reason about operating cost: tenant activity status to bound memory, index type chosen for small per-tenant shards, offboarding obligations, and the loss of cross-tenant queries.

for a principal

Own the irreversible call. Multi-tenancy is fixed at creation, so state the customer-count and skew assumptions, the isolation obligation, and the re-import cost of being wrong, before any data is loaded.

## Three shapes, one decision Every multi-customer deployment on Weaviate faces the same fork, and it is the kind of question a lead is expected to own because the choice is effectively permanent — whether a collection is multi-tenant is fixed at creation. ## Built-in tenants You opt in at creation with `multi_tenancy_config=Configure.multi_tenancy(enabled=True)`, optionally with automatic tenant creation on first write and automatic activation on access. Tenants are then managed through the collection handle — create, get, update and remove — and every data or query call goes through `collection.with_tenant("acme")`. A call that omits the tenant on a multi-tenant collection is an error, which is the point: isolation is enforced by the engine, not by remembering a filter. What you get: - **Physical separation.** Each tenant has its own shard and its own vector index. One customer's bulk import does not reshape another's graph, and their vectors never sit in the same search space. - **Cheap deletion.** Removing a tenant drops its shard. Compare that with deleting a million objects by filter, which is a slow, compaction-heavy operation — and matters for data-deletion obligations. - **A lifecycle.** Tenants can be active, inactive, or offloaded to cold storage. Inactive tenants release memory while keeping their data; activation happens on demand. In a product where a minority of customers are active on any day, this is the difference between paying for all of them and paying for the working set. - **Sensible index economics.** Because each tenant is a separate small index, a flat or dynamic vector index usually fits better than a full HNSW graph per tenant — the two decisions are made together. The costs: per-tenant shard metadata is real, so tens of thousands of tenants need attention to node memory and activation policy; cross-tenant queries are not a thing, so any global analytics needs a different path; and references cannot span tenants. ## A collection per customer This looks like stronger isolation and is usually worse. Collections are cluster-level schema objects, coordinated across nodes, so thousands of them inflate the schema and its coordination overhead in a way the tenant model is specifically designed to avoid. Worse, every schema change becomes an N-way migration — add a property and you apply it a thousand times, with partial-failure states. It is a defensible choice for a handful of very large, genuinely different customers, especially when their schemas actually differ or their data must live in separate deployments for compliance. It is not a general SaaS design. ## A tenant property and a filter One collection, a `tenant_id` property, and every query filtered by it. It is the simplest thing that works and it is right for some cases — few customers, small data, or a product where cross-customer search is a feature rather than a breach. Its weaknesses are systematic: - **No enforced isolation.** One forgotten filter is a data leak. The safeguard is code discipline, which fails eventually. - **Shared index.** All customers occupy one vector index, so a filtered search still works within a structure shaped by everyone's data, and a large customer degrades a small one's latency. - **Expensive offboarding.** Deleting a customer is a bulk delete plus cleanup, not a metadata operation. - **No per-customer economics.** You cannot deactivate an idle customer or offload them; everything is resident all the time. ## How to choose Ask how many customers, how skewed their sizes are, whether isolation is a compliance obligation or a preference, and how often customers are offboarded. Many customers plus a hard isolation requirement plus routine offboarding points squarely at built-in tenants. A handful of customers with modest data and no isolation obligation makes the filter approach honest and simple. Genuinely divergent schemas or separate compliance domains justify separate collections — or separate clusters. Then ask about the shape of the tail. If 90% of tenants are tiny and 10% are large, the tenant model plus a dynamic vector index and an aggressive deactivation policy gives you both economics and headroom. ## The trap to name explicitly Multi-tenancy cannot be switched on later. A team that ships with a `tenant_id` filter and plans to "migrate to tenants when we grow" is planning a full re-import of every customer's data under a new collection, with a cutover. Say that out loud in the design review, because that is the moment the decision is cheap.

  • How do you keep memory bounded when tenant count runs into the tens of thousands?
    Lean on the tenant lifecycle: keep only the working set active, mark idle tenants inactive so their indexes leave memory, and offload the truly dormant ones to cold storage, letting activation happen on access. Pair that with a small per-tenant index — flat, or dynamic so only large tenants build a graph — so each active tenant's resident cost stays low.
  • What does automatic tenant creation change about your ingest path?
    With it enabled, writing to an unknown tenant creates that tenant rather than failing, which removes an explicit provisioning step from onboarding. The trade is that a typo in a tenant name silently creates a real tenant holding real data, so you want the tenant identifier to come from a validated source such as an authenticated session claim rather than from client-supplied input.
  • How do you serve a query that must span all customers if you chose tenants?
    You do not get it from one query — tenant isolation means no cross-tenant search. Options are to fan out across tenants and merge in the application, which is fine for administrative reporting at modest tenant counts, or to maintain a separate non-tenant collection holding whatever global view you need. Deciding which global queries exist belongs in the design, before the model is fixed.
  • Why is a forgotten filter worse than it sounds in the tenant_id approach?
    It is a cross-customer data leak, not a bug that returns extra rows. Every query path, including ad hoc scripts, admin tools and future features, has to apply it correctly forever. Centralising access behind a wrapper that always injects the filter helps, but it is a convention rather than a guarantee, which is exactly the difference the built-in tenant model removes.

Tenants are separate rooms with their own locks; a tenant_id filter is one open-plan office where everyone is asked to only read their own desk.

saying these in an interview costs you the question

  • Plans to enable multi-tenancy after launch
  • Says a collection per customer scales to thousands of customers
  • Treats a tenant_id filter as equivalent isolation
  • Assumes cross-tenant queries work with built-in tenants
  • Forgets that queries must specify a tenant explicitly

context