Tenant isolation in vector search fails in three ways that do not exist in normal multi-tenant CRUD: approximate indexes apply your tenant filter after the scan (pgvector documents that a filter matching 10% of rows returns about 4 results at the default ef_search of 40), embeddings are reversible enough that a mis-scoped read is a content leak, and soft-deleted vectors stay reconstructible inside HNSW graphs after offboarding. Pick silo, pool, or bridge per layer using the AWS SaaS Lens definitions, enforce the tenant predicate above the query API rather than inside it, and prove isolation with a cross-tenant probe suite in CI instead of trusting a WHERE clause.
With HNSW and pgvector's default hnsw.ef_search of 40, a filter that matches 10% of your rows returns about 4 results. Not 40. Four. That is documented behavior in the pgvector README, not a bug report, and it is the fastest way to explain why multi-tenant retrieval is different from multi-tenant CRUD.
In a normal application, the tenant predicate and the index cooperate. You put tenant_id in a composite B-tree, Postgres pushes the predicate into the index scan, and correctness and performance arrive together. In vector search, the index is over the embedding column only. The approximate scan walks the graph, collects a fixed-size candidate set, and only then does your tenant filter apply. The boundary holds, but the results quietly thin out.
That is the mild failure. The severe one runs the other direction: a tenant identifier that arrives from the wrong place, a cached retrieval that outlives its session, an agent tool that takes tenant_id as an argument. Then the filter is not under-returning, it is returning someone else's data, and in B2B SaaS that is a breach notification rather than a support ticket.
This post is about the design decision underneath both: how you partition a vector store across tenants, what each pattern actually costs at scale, and the two failure modes that survive whichever pattern you pick.
Three ways vector search breaks isolation that SQL does not
Before choosing a topology, it is worth being precise about what you are defending against. Multi-tenant RAG has three failure modes with no clean equivalent in relational multi-tenancy.
The filter is a post-filter. Covered above, and worth restating because teams reach for it as the default design. In a shared collection, WHERE tenant_id = $1 is not a partition, it is a post-scan predicate against a candidate list the index chose without knowing about tenants. Correctness survives. Recall does not.
Embeddings are closer to ciphertext than to hashes. A shared index leaks content if it leaks vectors. The vec2text work recovered roughly 92% of 32-token inputs exactly from their embeddings, and the line of research kept getting more practical: ALGEN demonstrated few-shot black-box inversion from as few as a single sample with tested defenses proving ineffective, and a 2026 training-free attack worked cross-domain with query access alone. The design implication is blunt. If tenant B must not read tenant A's documents, tenant B must not read tenant A's vectors either, and "it's just floats" is not an isolation argument.
Deleting a vector does not remove it. In HNSW-backed stores, deletion is typically a tombstone. The node stays in the graph so traversal does not break, and the payload stays on disk until compaction. A 2026 study on soft-deleted embeddings reconstructed 25.5% of exact names from biography embeddings, 100% of structured medical attributes, and 99% top-1 on facial embeddings, all from vectors that had already been deleted. For a pooled index that means a churned tenant's data is still physically present in the same segments as your active customers.
Those three shape the rest of the decision. Pattern choice determines how much of each you inherit.
Silo, pool, and bridge, applied to a vector store
The vocabulary is worth using precisely because it already exists. The silo, pool, and bridge model comes from AWS's SaaS tenant isolation guidance, now maintained in the Well-Architected SaaS Lens, and it predates vector databases by years. It is not a vector-specific framework, which is exactly why it is the right one: tenancy is an architecture decision, and the vector store is one layer inside it.
Note the middle column. In silo, isolation is enforced by something outside your application: a different collection name, a different namespace, in the strongest form a different database. In pool, isolation is a line of your code. That difference is the entire argument, and it is why "just add a filter" deserves more scrutiny than it usually gets.
Bridge is also not a fence-sitting answer. AWS defines it as choosing the isolation model per layer rather than system-wide, and in practice that is what mature B2B products converge on: pooled retrieval for the long tail, dedicated resources for the enterprise accounts whose contracts demand it.
| Pattern | Vector store shape | Isolation boundary | Breaks at |
|---|---|---|---|
| Silo | One collection, index, or namespace per tenant | Infrastructure and storage: separate resources | Tenant count, onboarding latency, per-resource overhead |
| Pool | One shared collection, tenant as a payload or metadata field | Application code: the query predicate | A single bad code path |
| Bridge | Pooled by default, siloed for tenants that need it | Mixed, decided per layer | Complexity: two code paths to keep correct |
Pattern 1: Silo, and the ceiling nobody plans for
Collection-per-tenant is the pattern engineers reach for when the compliance conversation gets uncomfortable, because the boundary is visible and easy to explain to an auditor. It works, and it has a hard ceiling.
The vendors are unusually direct about this. Qdrant's multitenancy documentation recommends a single collection per embedding model with payload-based partitioning, and warns that running hundreds to thousands of collections per cluster produces resource overhead, cost increases, performance degradation, and cluster instability. Qdrant Cloud caps collections at 1,000 per cluster. Milvus documents defaults of 64 databases and 65,536 collections, and still recommends a partition key over collection-per-tenant for large tenant counts. Pinecone's documentation goes further and explicitly tells you to use namespaces instead of several indexes, with serverless index limits by plan that top out at 200 per project.
Read that table as a shape, not a scoreboard. The useful signal is that every vendor's recommended path for many tenants is a partitioned shared structure, and their silo units are capped in the low thousands or come with an explicit warning. If you sell to a few dozen enterprises, silo is fine and probably right. If you have a self-serve tier, silo has an expiry date on it.
Weaviate is the interesting exception because it treats tenancy as a first-class concept rather than leaving you to pick between collections and filters. Its tenants carry states: ACTIVE, INACTIVE (kept on local disk with memory freed), and OFFLOADED (moved to S3 cold storage, added in v1.26.0 via the offload-s3 module). That is the feature you actually want for a long tail of dormant tenants, because the cost problem with silo is rarely the active accounts. It is the 80% that signed up and went quiet while still holding resident memory. The docs describe reactivation qualitatively as fast from INACTIVE and slow from OFFLOADED, without published latency figures, so budget for a cold-start path in your UX and measure it on your own data.
| Store | Silo unit | Documented limit | Vendor guidance |
|---|---|---|---|
| Qdrant | Collection | 1,000 per Cloud cluster | Prefer one collection with payload partitioning |
| Pinecone | Namespace (not index) | 100 to 100,000 namespaces per index by plan; 5 to 200 indexes per project | Namespaces over multiple indexes |
| Weaviate | Tenant (native multi-tenancy) | No documented tenant ceiling | Native multi-tenancy with tenant states |
| Milvus | Collection or partition | 65,536 collections, 1,024 partitions per collection (configurable) | Partition key for large tenant counts |
Pattern 2: Pool, and making the filter do real work
Pooled is the default for good reasons: one index, one set of operational knobs, linear cost, instant onboarding. It also concentrates all of your isolation risk into query construction. Two things make it defensible.
Give the tenant field a real index, not just a WHERE clause. Qdrant added exactly this in v1.11.0: a keyword payload index created with is_tenant=true, which tells the engine to co-locate a tenant's vectors on disk instead of scattering them across the collection.
client.create_payload_index(
collection_name="documents",
field_name="tenant_id",
field_schema=models.KeywordIndexParams(
type="keyword",
is_tenant=True,
),
)The point is not the API call, it is what it changes: the tenant predicate stops being a filter over a global candidate set and starts being a locality hint the index respects. Qdrant extended this in v1.16 with tiered multitenancy, where large tenants get promoted from a shared fallback shard to dedicated shards past a size threshold. That is bridge, implemented inside the database. No before-and-after numbers are published for it, so treat it as a capability to evaluate rather than a promised speedup.
On pgvector, fix the post-filter explicitly. The under-return is not theoretical and it gets worse the more tenants you have, because each tenant's share of the index shrinks. Since pgvector 0.8.0 (released October 2024) there is a supported answer:
SET hnsw.iterative_scan = strict_order; SET hnsw.max_scan_tuples = 20000; SELECT id, chunk FROM documents WHERE tenant_id = $1 ORDER BY embedding <=> $2 LIMIT 10;
strict_order keeps results in exact distance order and scans further into the index until it has enough matching rows. relaxed_order is faster and allows slight reordering. IVFFlat supports relaxed_order only, with ivfflat.max_probes as its bound. The scan-limit parameters exist so a very selective tenant filter cannot turn one query into a full index walk, which is the failure you are trading into. Set them deliberately. If you are already tuning HNSW for scale, the parameter interactions are covered in our guide to pgvector HNSW tuning for 10M+ rows.
For very large deployments, native Postgres partitioning by tenant_id with a per-partition HNSW index beats both options, because the ANN scan then runs inside a single tenant's data and the filter disappears entirely. That is a bridge pattern wearing a pool costume, and it is usually the right endgame.
The filter is not the control plane
Here is the part that matters more than topology. In most pooled systems, the tenant identifier travels as a parameter: pulled off a request, passed down a call chain, eventually interpolated into a search call. Every hop is a place it can be wrong, and RAG adds hops that traditional apps do not have. Retrieval caches. Rerankers. Evaluation harnesses. Agent tools whose arguments are chosen by a model.
A 2026 paper on multi-tenant enterprise retrieval measured this directly and reported 62% to 100% cross-tenant leakage under adversarial probing when retrieval was ungated, with attribute-based gating eliminating it in their tests. The number to internalize is not the ceiling, it is that ungated retrieval failed at all under probing that a determined user could reproduce.
Three rules follow, and they are cheap:
SET LOCAL app.tenant_id plus row level security, or a repository wrapper that owns the filter, so the unscoped query is not expressible in application code. RLS and ANN under-return are separate problems: RLS gives you a boundary the application cannot forget, iterative scans give you your k results back. You need both, and the broader pattern is the same one we describe for role-based access control in AI applications.Worth noting what this does not cover. EchoLeak (CVE-2025-32711), the zero-click prompt injection against Microsoft 365 Copilot disclosed in June 2025, exfiltrated data through the RAG grounding pipeline. It was single-tenant exfiltration, not a cross-tenant index breach, and it is often miscited as the latter. It belongs in your threat model as evidence that the grounding path is an attack surface, not as evidence that shared indexes leak between customers.
What offboarding actually leaves behind
Deletion is the isolation problem that only shows up after the contract ends, and it is the one most teams have never tested.
The mechanics are the same across HNSW implementations. Removing a node from a proximity graph would sever the edges other nodes rely on for traversal, so deletes are tombstoned instead. The vector stays reachable in the graph structure and present in the segment on disk until compaction, merge, or optimizer passes reclaim it. In a pooled index, that means a churned tenant's embeddings sit in the same segments as your active customers, recoverable by anyone who can read the storage layer, for as long as compaction has not run.
The Ghost Vectors study put figures on what recovery looks like: 25.5% exact-name recovery from biography embeddings, 100% of structured medical attributes, and 99% top-1 accuracy on facial embeddings, from soft-deleted vectors. If you have a GDPR erasure obligation, a DELETE call is a request, not a guarantee, and the gap between the two is your exposure window. Our guide to handling EU customer data covers the wider obligation, but the vector-specific mechanics are what break the assumption.
A workable offboarding sequence:
Step five is where most of the residual data actually lives.
The decision, and how to prove it holds
The topology choice is less interesting than it looks once the enforcement layer is right. A sane default:
Whichever row you land on, the isolation claim needs a test that runs on every deploy, because none of these patterns fail loudly. Seed two synthetic tenants with distinctive canary documents. For every retrieval entry point in the product, query as tenant A with terms that only match tenant B's canaries and assert zero hits. Then extend the suite to the paths that skip the normal request flow: cached retrievals, reranking, agent tool calls, admin impersonation, background jobs, and the evaluation harness itself. Add a deletion probe that offboards a tenant and re-queries its canaries after compaction.
That suite belongs next to your retrieval quality tests, not in a separate security backlog. If you are already running RAG evaluation in CI, cross-tenant probes are a handful of extra assertions on infrastructure you have. And once tenants are properly scoped, the same tenant identifier that gates retrieval is the one that makes per-tenant LLM cost attribution possible, which is the rare case where a security control pays for itself in finance reporting.
If you are still choosing the store underneath all this, our comparison of Pinecone, Weaviate, and Qdrant covers the operational tradeoffs, and the wider security context lives in our AI security pillar.
The summary is short. Vector stores give you a filter and let you believe it is a boundary. Put the boundary somewhere your application cannot forget it, assume the embeddings are readable text, assume deletes have not happened until you have verified the count, and run the probes.
| Situation | Pattern |
|---|---|
| Self-serve or long tail, thousands of tenants | Pool, with a tenant-aware payload index or per-tenant Postgres partitions |
| Regulated tenants, contractual data residency, or per-tenant key destruction | Silo for those tenants only |
| Mixed book of business (most B2B SaaS) | Bridge: pooled default, promotion path to dedicated resources |
| Under ~50 tenants, all enterprise | Silo, and revisit when self-serve ships |
FAQ
Quick answers to the questions this post tends to raise.




