
Qdrant supports two common multi-tenant layouts: a shared collection with indexed tenant payloads, and separate collections or shards for workloads that require stronger physical isolation. The right choice depends on tenant count, workload skew, compliance boundaries, and operational overhead. This article compares both patterns and places them alongside pgvector, Pinecone, and Weaviate so the isolation decision is explicit and reviewable.

Multitenancy in Qdrant has a precise meaning: keeping the data of different tenants — end users, clients, or entire organizations — isolated while everything lives inside a single cluster. When I finally sat down and mapped the design space after my collection-per-tenant mistake, I realized there are exactly two strategies available. Every multi-tenant architecture built on Qdrant is some variation of one of these:
Qdrant's official guidance is blunt about which one should be your default: avoid creating multiple collections unless you truly need them. The reasoning is operational, not philosophical. Sharding is the mechanism for scaling across nodes, while multitenancy — the tagging pattern — handles different use cases within the same infrastructure. Together they let you scale without unnecessary collection-management overhead.
A collection is a set of points you can search over together, and it enforces two hard rules: vectors of the same name must share the same dimensionality, and they must all be compared with a single distance metric. In a shared collection this becomes a real planning constraint, because every tenant is locked into the same embedding model:
If Tenant A stores 1024-dimensional vectors, Tenant B's data has to fit that same vector space — or it cannot live in the collection at all. A tenant that insists on a different embedding model is one of the few cases where splitting is genuinely justified.
Here is where I see teams get burned. Production RAG architecture guides warn that simply putting a tenant identifier into point metadata and filtering during retrieval is not sufficient. Isolation can break silently through several documented failure modes, and by the time anyone notices, one customer's documents may already be appearing in another customer's answers.
The stakes are concrete. Agent memory must never cross customer boundaries, and authorization has to be enforced outside the LLM prompt — never by trusting the model to filter its own results. A model that quietly ignores half of the retrieved context is a behavior, not a security control.
That is why I now treat tenancy the way Qdrant's guidance implies: a deliberate design decision made before the first tenant is onboarded, not a flag you flip when customer number two signs. Once real customer data sits inside one pattern, migrating to the other only gets more expensive.

The pattern that replaced my collection sprawl is almost embarrassingly simple: one collection, one extra payload field. Every vector carries its tenant identity from the moment of upsert:
client.upsert(
collection_name='tenant_data',
points=[models.PointStruct(
id=2,
payload={'group_id': 'tenant_1'},
vector=[0.1, 0.9, 0.1]
)],
shard_key_selector='canada'
)
Two details stand out. The group_id tag lives inside the payload, so tenant separation becomes a property of the data itself, not of the infrastructure. And the shard_key_selector routes this point to the canada shard, which means I can still control physical placement for specific tenants without handing each one a dedicated collection. Infrastructure is shared; the data stays logically separated.
The order of operations decides whether this pattern performs at all:
client.create_payload_index(
collection_name='documents',
field_name='tenant',
field_schema='keyword'
)
Whether you call the field group_id or tenant doesn't matter — consistency does. The reason the order matters is architectural: the payload index extends the HNSW graph, so the optimizer only needs to build that graph once. If the index exists before ingestion, tenant awareness is baked in from the start. Create it after ingestion and Qdrant is forced to rebuild the graph over everything you just stored.
This is the machinery behind the surprise I mentioned earlier. Qdrant's Filterable HNSW applies filter conditions during the HNSW graph traversal, not as a post-filter step. Contrast that with the classic approach: fetch the top-k candidates, then throw away every point that fails the tenant check. With a selective tenant filter, most of that expensive search work produces garbage.
In Qdrant, the candidate set is constrained during the graph search itself, so filtering does not degrade search performance. A realistic tenant query stacks several conditions into one filter:
Filter(must=[
FieldCondition(key='user_id', match=MatchValue(value=42)),
FieldCondition(key='category', match=MatchValue(value='technical')),
FieldCondition(key='created_at', range=Range(gte=1700000000))
])
That's an equality match on user_id, an equality match on category, and a range condition on created_at — all three evaluated during graph traversal, which is why recall stays high even when the combined filter is highly selective. The operator vocabulary covers the rest of the business logic:
And payloads support keyword matching, full-text filtering, numerical ranges, and geo-locations, so access rules, document categories, and location constraints can all ride on top of a single similarity search.
One shared collection means one shared blast radius, which is where strict mode earns its keep. It protects the cluster from suboptimal usage by:
Here's the part I would flag in any production review: the OSS version enforces none of this by default. Without strict mode configured, a single careless tenant query can tie up the whole cluster — so treat enabling it as part of the deployment checklist, not an optional hardening step.
The embedding layer has one rule of its own. Cohere Embed v3, released in November 2023, requires an input_type parameter on every API call, and the two values are not interchangeable:
For multilingual tenants, embed-multilingual-v3.0 covers 100+ languages, so a single shared collection can absorb a globally mixed tenant base without per-language collections either.
That is the entire pattern: one collection, one payload index, one filter attached to every query. It looks almost too simple to be trustworthy — which is exactly why the collection-per-tenant approach still has defenders. In the next section, I'll show you where they have a point.

A dedicated collection is justified when a tenant needs a different embedding model, stronger physical isolation, independent snapshots and restore procedures, or protection from a sustained noisy-neighbor workload. It also reduces the blast radius of schema changes and makes tenant offboarding a discrete lifecycle operation. The trade-off is operational: every collection adds monitoring, backup, index, and capacity-planning work.

The group_id tag solved my logical isolation problem, but it couldn't answer the one question my compliance review kept asking: where do these vectors physically live? With a plain shared collection, the honest answer is "somewhere in the cluster" — and that answer collapses the moment a tenant demands GDPR-grade data residency. This is where Qdrant's multitenancy story goes beyond logical filters.
Qdrant's answer is custom sharding, also called user-defined sharding: for multitenant workloads, you route data to specific shards using a shard key. In the backend, Tenant 1's data can live in Shard 1, hosted in Canada, while Tenant 2's data sits in Shard 2, hosted in Germany — physically separated, in different jurisdictions, yet still inside the same collection and the same infrastructure. What impressed me most is how little ceremony it takes: the same upsert that tags group_id also routes placement via shard_key_selector='canada', so tenant identity and physical location are decided at write time, in one call. You get precise control over data distribution, performance isolation, and tenant separation without falling back into the dozens-of-collections mess I was trying to escape.
Replica sets manage copies of shards across nodes, so 4 shards with a replication factor of 2 yield 8 physical shards spread across the cluster. A replication factor of at least 2 is strongly recommended for production, and the feature list explains why: high availability, automatic failover, zero-downtime rolling upgrades, and rolling restarts that never take your collection offline.
Taken together, custom sharding is what made the shared-collection model defensible to my security team: group_id gives me logical isolation, shard keys give me physical residency, and I still operate one collection instead of dozens. That said, this model isn't universal — some tenant requirements break it entirely, and that's where a collection per tenant comes back into play.

Whatever collection strategy you settled on, it only protects data at rest inside the database. The failures that actually embarrass engineering teams happen one layer up, in the retrieval pipeline that wraps the vector search. Qdrant's tenant predicates run inside the HNSW traversal, but that guarantee covers exactly one search call — everything your code does before, beside, or after that call is your responsibility. Production RAG architectures document three failure modes here, plus a fourth that is purely human. What strikes me about this list: not one of these bugs lives inside the vector database.
Hybrid search means two code paths, and two code paths mean two chances to get tenancy wrong. If your vector leg applies the tenant filter correctly but your keyword leg runs a separate query builder without the same predicate, keyword matches can pull documents straight across tenant boundaries. Your vector tests all pass; the leak appears only when a sparse term happens to hit another tenant's corpus.
Rerankers are a quieter leak. Most models score the whole candidate batch at once, and unless the batch is scoped to a single tenant before inference, cross-tenant interactions can influence the scores. The mundane version is just as dangerous: an off-by-one error in the result-extraction step pairs a high score with the wrong payload, and you confidently return another tenant's document.
This one degrades quality even when every filter is perfect. Picture one tenant uploading 100,000 documents while another uploads 1,000. The large tenant occupies most of the index, so approximate nearest neighbor search spends most of its traversal in their vectors before it ever reaches the small tenant's neighborhood. The small tenant gets worse recall and latency — nobody leaked anything, the index geometry itself is skewed. It is the strongest argument I know for putting very large tenants on dedicated collections.
Tutorials teach you to scope queries with metadata filters like where: { userId: 'abc' }, and that works right up until one code path forgets the filter — at that moment, users start seeing each other's documents. This is exactly why Pinecone separates tenants into namespaces physically: every upsert, query, and delete is explicitly scoped to the user's namespace, so tenancy is a property of the request rather than a clause you can accidentally drop.
A related trap hurts answers rather than safety. Retrieve the top 100 candidates and filter by permissions afterwards, and you may discard 95 of them as inaccessible — leaving a handful of usable chunks and an under-retrieved, weak response. Permission predicates belong inside the search itself, so ranking operates only on content the user can actually read.
You cannot review tenancy rules by eyeball on every pull request, so encode them as policy-as-code. A Rego rule running under Open Policy Agent can:
The rule is version-controlled, testable, and auditable — a compliance change becomes a pull request instead of a Slack conversation. Pair it with two habits: log every retrieval event with user, timestamp, query, and documents accessed, and propagate role-based access control from your source systems into the vector index, so nobody can surface content through RAG that they could not open upstream. Isolation is not a single filter; it is a chain, and every link has to hold.

Qdrant's baseline is worth restating before the comparison, because it's what I measure everything else against: a single collection, the tenant stored as an indexed payload field, and a keyword filter on that field that runs inside the HNSW traversal rather than after it. What surprised me is how differently the other major engines arrive at the same goal — and how much the "right" answer depends on where each one draws its isolation boundary.
pgvector has no tenancy features of its own. Isolation comes entirely from PostgreSQL's Row-Level Security (RLS): one table, one HNSW index, a tenant_id on every row, and a policy that silently restricts every query to the current tenant.
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.current_tenant')::UUID);
When I trace how a query behaves here, it's the closest cousin to Qdrant's model — isolation enforced at query time inside one shared structure. The stack needs PostgreSQL 13+ and pgvector 0.7+, and the HNSW defaults are solid out of the box: m=16 and ef_construction=64. Raising ef_construction improves recall but slows index builds. This pattern holds up well for dozens to a few hundred tenants, and it's the simplest model to reason about if your team already lives in SQL — the isolation logic sits in the same database they debug every day.
Pinecone takes the opposite bet: isolation is explicit and absolute. Each tenant gets a namespace, and every upsert and query is scoped to exactly one:
index.upsert(vectors=batch, namespace='tenant-acme-corp')
index.query(vector=q, namespace='tenant-acme-corp')
You cannot query across namespaces in a single API call. The moment a product needs cross-tenant retrieval — admin dashboards, deduplication — you're signing up for multiple calls plus client-side merging, and that glue code ages poorly. The quotas narrow your options too: standard plans allow 100,000 namespaces but only 20 indexes, and per-namespace performance degrades as tenant count grows, so the practical ceiling arrives before the quota does.
Weaviate is the only engine here with multi-tenancy built directly into its data model, but you pay in ceremony. Tenants must exist before you ingest anything:
client.collections.create(
name='Document',
multi_tenancy_config=Configure.multi_tenancy(enabled=True)
)
documents.tenants.create(['acme-corp', 'other-client'])
The schema must also be fully typed upfront — no schema-on-write. The payoff is structural: each tenant gets its own shard within the collection, which means strong isolation at both the logical and physical storage layer. Operationally, that buys you:
To me, this reads as collection-per-tenant with the management pain automated away — a middle position between Qdrant's shared graph and Pinecone's namespace walls.
The 2026 workload frameworks converge on two useful heuristics. Classic multi-tenant SaaS maps cleanly onto Weaviate's native multi-tenancy or Pinecone. Latency-critical workloads under 5ms p99 point to Qdrant as the unambiguous winner, because that in-graph filter is doing the heavy lifting. None of these four approaches is wrong — the honest answer is that the choice comes down to your tenant count and which skill set your team already trusts.

Tenant isolation needs an explicit lifecycle for deletion, recovery, and architecture changes. Treat these operations as production workflows rather than one-off database commands.
Create a collection snapshot before large payload migrations, re-sharding, or destructive cleanup, and test restoration in a separate environment. A snapshot that has never been restored is an assumption, not a recovery plan.
Payload-only changes can be updated without regenerating embeddings. A change to embedding model, vector dimension, or distance metric should be handled as a new collection or controlled re-index so incompatible vectors are never mixed in one search space.
## Methodology and sources
- **Reviewed:** September 4, 2026.
- **Method:** Architecture comparison based on official database documentation. Capacity and latency figures in this article are planning heuristics, not measurements from one controlled workload.
- **Primary sources:** [Qdrant multitenancy guidance](https://qdrant.tech/documentation/guides/multiple-partitions/), [Qdrant distributed deployment](https://qdrant.tech/documentation/guides/distributed_deployment/), [pgvector](https://github.com/pgvector/pgvector), [Pinecone namespaces](https://docs.pinecone.io/guides/index-data/implement-multitenancy), and [Weaviate multi-tenancy](https://docs.weaviate.io/weaviate/manage-collections/multi-tenancy).
- **Limitations:** Tenant count alone does not determine the design. Benchmark filtered recall, p95/p99 latency, delete time, backup/restore, and noisy-neighbor behavior using your own vector distribution and hardware.