Skip to content
Aditya MhaskeSoftware Engineer

Articles / Multi-tenant isolation

Systems · field study · 28 June 2026

Isolation is four dials.
Everyone ships it as one switch.

A field study of multi-tenant isolation models for retrieval systems — why the relational playbook misprices them, where filtering quietly stops being isolation, and the four-axis model I use to pick a configuration instead of a slogan.

Abstract

Multi-tenancy is usually taught as a single decision with three answers: give every tenant their own stack (silo), share one stack between all of them (pool), or split the difference (bridge). That framing was built for relational systems, where the marginal cost of a tenant is rows on disk and the argument for separation is mostly regulatory.

Retrieval systems break the framing. In a vector store the dominant resident cost is not the data — it is the approximate-nearest-neighbour index built over the data, and that index is rebuilt in full for every partition you create. Isolation stops being a compliance conversation and becomes a capacity-planning one.

This piece argues that isolation is not one dial but four independent ones — storage, compute, enforcement, and operations — and that the useful architectures are the ones that set those dials to different values. It ends with the configuration I run in production across 100+ tenant environments, the measurements that justified it, and the three things it cost me.

Key findings

  1. 01Isolation is four independent axes, not one slider. The canonical silo/pool/bridge patterns are just the diagonal of a four-dimensional space; the defensible designs are usually off-diagonal.
  2. 02In vector retrieval the per-partition fixed cost dominates below roughly 50k vectors per tenant. Above it, per-tenant indexes are affordable; below it, they are the single largest line item in your memory bill.
  3. 03Pooled storage does not require pooled enforcement. Storage isolation is an efficiency decision; enforcement isolation is a security decision. Conflating them is how pooled architectures get their bad reputation.
  4. 04Filtering is not free isolation. A tenant predicate over a shared proximity graph degrades recall in a way that scales with how selective the predicate is — the failure is silent and does not show up in latency graphs.
  5. 05Operational isolation is the axis that gets discovered last and hurts most. Per-tenant restore, per-tenant erasure, and per-tenant cost attribution are trivial in a silo and are genuine engineering projects in a pool.

4

Isolation axes

storage · compute · enforcement · ops

100+

Tenant environments

single pooled collection

80%

Resident memory saved

vs. collection-per-tenant

~50k

Vectors/tenant crossover

below it, pooling wins on cost

1,000

Collections per cluster

vendor default ceiling [6]

3

Things it cost me

documented in §9

§2 · The model

Four questions,
asked separately.

Teams pick a word — pool, silo — and inherit four decisions from it. Ask them one at a time and most of the interesting architectures turn out to be the ones that answer them differently.

Axis A

Storage & index

Where do a tenant's bytes live, and who else's bytes share the structure built over them?

The axis everyone means when they say 'isolation'. In a relational system this is about rows and schemas. In a retrieval system it is about the proximity graph — because the graph, not the payload, is what sits resident in RAM.

A0

Shared index, tenant as an attribute

One collection, one graph. Tenant identity is a payload field, applied as a predicate at query time. Cheapest possible configuration and the default recommendation for large fleets of small tenants [6].

1 graph · O(1) fixed overhead

A1

Shared index, tenant-aware physical layoutshipped

Still one logical index, but the engine is told which field is the tenant key and co-locates each tenant's vectors so a scan reads sequentially instead of scattering. Qdrant exposes this as `is_tenant=true` on the payload index [6].

1 graph · + layout metadata

A2

Dedicated index, shared process

Each tenant gets its own shard/namespace/graph inside a shared cluster. Pinecone's serverless namespaces and Weaviate's shard-per-tenant model both land here [8][10]. Isolation is physical; the process, node, and control plane are still shared.

T graphs · O(T) fixed overhead

A3

Dedicated database or cluster

A full stack per tenant. The only configuration where a software defect in the query path cannot return another tenant's data, because the other tenant's data is not reachable from the process.

T clusters · O(T) everything

Why A1 in production

Tenants averaged well under the crossover point. Per-tenant graphs would have spent 80% of the memory budget on fixed overhead for partitions holding a few thousand vectors each.

§3 · Coordinates

Silo and pool are
two points on a diagonal.

Once isolation is four numbers instead of one word, the canonical patterns stop being categories and become coordinates — and the space between them stops being a compromise. AWS already concedes this: the bridge model is described as the common case, not the fallback [5].

A · storage
0
1
2
3
B · compute
0
1
2
3
C · enforcement
0
1
2
3
D · operations
0
1
2
3

The off-diagonal configuration this article argues for: share the index because sharing is cheap, and over-invest in enforcement because enforcement is what pooling actually costs you.

Fits

Large fleets of small-to-medium tenants where per-partition overhead dominates, and where a leak is existential but a shared graph is not.

Breaks

Requires discipline that does not survive staff turnover unless it is encoded in types and policies rather than in review culture.

Fig. 1The same three tenants under each topology
SiloA3 · B3 · C3 · D3T1T2T3PoolA0 · B0 · C1 · D0T1T2T3BridgeA2+A0 · B2 · C2 · D1T1T2T3svcindexsvcindexsvcindex3 tenants → 3 of everythingshared svcone index · tenant_id3 tenants → 1 of everythingshared svcsvcpooled indexindextail pooled · T3 pinned
Silo replicates the whole stack per tenant; pool collapses it to one and demotes tenancy to a predicate; bridge pins the tenants that earn it and pools the rest. Note that the diagram says nothing about where the tenant scope is enforced — that is an independent axis, and the reason two systems with identical topology can have very different security properties.

The one claim to take away from this section

Storage isolation and enforcement isolation are answers to different questions. The first asks what it costs to keep tenants apart; the second asks how many independent mechanisms must fail before they aren’t. Pooling the first while siloing the second is not a compromise between them — it is the configuration that gets you the cost profile of a pool and the failure profile of a silo.

§4 · Economics

The relational playbook
misprices retrieval.

Salesforce has run thousands of tenants against one shared schema for two decades, scoping every operation by an org identifier [13]. That works because in a relational engine a partition is nearly free. In a vector engine a partition is an entire ANN index, and the arithmetic inverts.

Fig. 2Splitting a corpus does not divide the cost — it multiplies the overhead
A2 · one index per tenant12 partitions × fixed overheadcollapseA1 · one shared index1 partition × fixed overheadsame vectors, one graphvector payload — invariant to partitioningper-partition fixed overhead — linear in partition count
Vector payload is invariant to partitioning: the same embeddings occupy the same bytes however you group them. Per-partition overhead is not — it is paid once per partition, so it scales with tenant count while buying no additional capacity. Below the crossover point, the dark band is the majority of the bill.

4.1

In relational systems the marginal cost of a partition is near zero. In vector systems it is not.

A schema-per-tenant Postgres database adds catalog entries and a connection pool. A collection-per-tenant vector store adds an entire ANN index: its own graph, its own segments, its own identifier and version trackers, its own optimiser working set. The data is the small part.

4.2

HNSW's memory cost is structural, not incidental.

The index is a multi-layer proximity graph in which each node keeps up to m outbound links, and roughly 2m at the base layer [1]. Every link is a 4-byte identifier. The graph therefore costs on the order of m × n × 4 bytes over and above the vectors themselves — and it is rebuilt in full, per partition, every time you split the corpus.

graph ≈ 1.5 · m · n · 4 bytes

4.3

Below a crossover point, per-partition fixed overhead is the largest line item in the bill.

Fixed per-collection cost is constant in tenant size but linear in tenant count. Vector payload is linear in total corpus size regardless of how you split it. There is therefore a corpus size per tenant below which splitting costs more than the data you are splitting. In the 768-dimension regime that crossover sits in the tens of thousands of vectors per tenant.

silo − pool ≈ T · F

4.4

Vendors encode this in their defaults, and in hard ceilings.

Qdrant's documentation states plainly that 'creating a separate collection for each tenant is rarely the most efficient approach' because 'each collection carries its own resource overhead', and caps Cloud clusters at 1,000 collections by default [6]. Pinecone and Weaviate reach the opposite conclusion by making their per-partition unit cheap enough to hand out a million times — namespaces and per-tenant shards respectively [8][10].

1,000 collections · 1M+ namespaces

4.5

Which means 'per-tenant index' is not one architecture — it is two, with different economics.

A per-tenant Qdrant collection and a per-tenant Pinecone namespace both read as A2 on the storage axis, but the first is a heavyweight object with an explicit fleet ceiling and the second is designed to be created a million times. Read your engine's per-partition cost before you read anyone's architecture diagram, including this one.

§5 · The silent failure

A filter is not
a partition.

If you pool, tenancy becomes a predicate. In a B-tree that is a cheap lookup. In a proximity graph it is a connectivity problem — and it fails by returning worse answers, not by returning errors, which is the worst way for anything to fail.

Fig. 3A tenant predicate can disconnect the eligible subgraph
Tenant filter over a standard HNSW graphentrytrue NNstranded✕ returns k results · none of them the nearest neighbourFilter-aware index — extra links between eligible pointsentrytrue NN✓ traversal reaches the true nearest neighbour
Left: HNSW built its links on geometry alone, so the paths between one tenant's points run through other tenants' points. Apply the filter and those paths vanish; the walk strands at its entry point and returns whatever it already had. Right: a filter-aware index adds links between points sharing an indexed value, so the eligible subgraph stays traversable. Same data, same query, different recall — and identical latency, which is why this never shows up on a dashboard.

5.1

A tenant predicate over a shared graph is a graph-connectivity problem, not a WHERE clause.

HNSW answers a query by greedily walking a proximity graph. Apply a tenant filter and most nodes become ineligible mid-walk — but they were the edges connecting the eligible ones. The traversal can strand itself in a region of the graph with no remaining path to the true neighbours. Qdrant states the failure directly: 'vector search can't cross the grayed out area and it won't reach the nearest neighbor' [7].

5.2

The failure is a recall failure, so your latency dashboards stay green.

This is the property that makes it dangerous. The query returns quickly and returns k results. They are simply worse results than the index could have produced, for the tenants whose slice of the corpus is smallest — which are usually your newest customers, evaluating you during a trial.

5.3

Naive mitigations fail at opposite ends of the selectivity range.

Post-filtering searches globally then discards non-matching hits: with a selective tenant predicate you discard nearly everything and return short. Pre-filtering restricts the candidate set first: correct, but degenerates toward a brute-force scan as the tenant grows. Neither is uniformly right, which is why the choice has to be made per query rather than per system [7].

5.4

The production answer is a cardinality-aware planner over a filter-aware index.

Estimate how many points the predicate admits, then pick a strategy: below a threshold, retrieve directly through the payload index and skip the graph; above it, traverse a graph that has been built with the filter in mind. Qdrant's filterable HNSW adds extra links between points sharing an indexed payload value so the filtered subgraph stays connected, and switches strategies on estimated cardinality against a full-scan threshold [7].

planner switch on estimated cardinality

5.5

This is an active research area, not settled engineering.

Filtered-DiskANN builds label-aware connections at index construction time, forming edges from the geometry of the vectors and the associated label sets together [2]. ACORN takes the opposite tack — predicate-agnostic subgraph traversal over a standard HNSW, reporting 2–10× higher throughput on low-cardinality predicates and over 30× on complex high-cardinality ones [3]. Both exist because filtering a proximity graph is genuinely hard.

[2] WWW '23 · [3] SIGMOD '24

5.6

Corollary: never ship pooled retrieval without a per-tenant recall test.

Freeze a labelled evaluation set per tenant size decile. Measure Recall@k against exhaustive search — not against your own previous release. The smallest decile is the one that will regress, and it is the only one that will tell you the planner's threshold is set wrong.

collection setup · declaring the tenant key
# A1 — declare the tenant key to the engine at collection-creation time.
# Without this index the planner cannot estimate filter cardinality and
# cannot choose a strategy; with is_tenant it also co-locates on disk. [6][7]

client.create_payload_index(
    collection_name="documents",
    field_name="tenant_id",
    field_schema=KeywordIndexParams(
        type="keyword",
        is_tenant=True,      # co-locate this tenant's points → sequential reads
    ),
)

§6 · The boundary

Pool the bytes.
Never pool the boundary.

In a silo, a bug in the query path returns nothing, because the other tenant's data is not reachable from the process. In a pool, the same bug returns everything. That asymmetry is the real price of pooling, and it is payable in engineering rather than in RAM.

Fig. 4Independent mechanisms, each of which must fail
A cross-tenant read must defeat every layer below itC0Client sends tenant_idstops: nothingL1Scope derived from verified token claimstops: forged / enumerated tenant idsL2Repository exposes no unscoped querystops: the forgotten WHERE clauseL3Row-level security on a per-tenant rolestops: a wrong or compromised app tierL4Continuous cross-tenant probe in prodstops: silent regressions in L1–L3attempted cross-tenant read
The layers are deliberately heterogeneous — a token claim, a type signature, a database policy, and a live probe fail for different reasons, so a single class of mistake cannot take out more than one. C0 is included because it is common, not because it is a layer.
LayerMechanismStopsStill fails if
L1Identity, not inputTenant scope is read from a verified token claim at the edge and attached to a request context. No handler accepts a tenant identifier as a parameter.Forged or enumerated tenant identifiers in the request body — the entire IDOR class.A background job or admin path that constructs its own context and gets it wrong.
L2Unrepresentable, not merely forbiddenThe repository exposes no method that returns unscoped results. Scope is a constructor argument, not an optional filter. The escape hatch exists, is named for what it is, and is greppable in CI.The forgotten `WHERE tenant_id = ?`, in the hands of a developer who has never read this document.Anyone who reaches past the repository to the driver.
L3A second mechanism the app cannot bypassPer-tenant database credentials plus row-level security policies bound to the session role, so the engine re-derives the scope independently of the query [11]. Or a centralised authorisation service in the Zanzibar lineage, which at Google served trillions of ACLs under 10 ms at p95 [12].A compromised or simply wrong application tier.Superuser and BYPASSRLS roles bypass row security entirely [11] — so the service role must never hold either, and that must be asserted in a test, not a runbook.
L4Proof, not assertionA cross-tenant probe running continuously in production: tenant A's credentials issued against tenant B's known-present document identifiers, asserting empty results and alerting on any hit.Silent regressions in every layer above, including ones introduced by a dependency upgrade.Nothing, if it is genuinely running. Everything, if it was disabled during an incident and never re-enabled.
✕ the shape every tutorial ships
# C0/C1 — the shape almost every tutorial ships.
# The filter is a keyword argument, which means it is optional,
# which means it is one refactor from being absent.

def search(query: str, tenant_id: str, limit: int = 10):
    return client.query_points(
        collection_name="documents",
        query=embed(query),
        query_filter=Filter(must=[
            FieldCondition(key="tenant_id", match=MatchValue(value=tenant_id))
        ]),
        limit=limit,
    ).points

# Three ways this leaks:
#   1. tenant_id arrives from the request body instead of the token
#   2. a new call site omits query_filter and gets the whole corpus
#   3. an aggregate/scroll/recommend path is added without the filter
✓ scope as a property, not a parameter
# C2 — scope is a property of the object, not an argument to the call.
# There is no method on this class that can return another tenant's points.

class TenantScopedIndex:
    """The only sanctioned door to the collection.

    Constructed from a verified token claim, never from request input.
    Direct QdrantClient use outside this module is blocked in CI.
    """

    _COLLECTION = "documents"

    def __init__(self, client: QdrantClient, claims: VerifiedClaims):
        # Not a parameter the caller chooses — a claim the edge verified.
        self._client = client
        self._tenant = claims.tenant_id

    def _scope(self, extra: Filter | None = None) -> Filter:
        must = [FieldCondition(key="tenant_id",
                               match=MatchValue(value=self._tenant))]
        if extra and extra.must:
            must.extend(extra.must)
        return Filter(must=must)          # tenant clause cannot be displaced

    def search(self, query: str, limit: int = 10, where: Filter | None = None):
        return self._client.query_points(
            collection_name=self._COLLECTION,
            query=embed(query),
            query_filter=self._scope(where),
            limit=limit,
            # Tenant-aware layout: co-locates this tenant's vectors so the
            # filtered walk reads sequentially instead of scattering. [6]
            search_params=SearchParams(hnsw_ef=128),
        ).points
C3 · a second mechanism the application cannot bypass
-- C3 — a second enforcement mechanism, below the application.
-- The engine re-derives tenant scope from the session, so a missing
-- WHERE clause in application code returns zero rows instead of everyone's. [11]

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE  ROW LEVEL SECURITY;   -- applies to the owner too

CREATE POLICY tenant_isolation ON documents
    USING      (tenant_id = current_setting('app.tenant_id')::uuid)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

-- Non-negotiable, and asserted by a test rather than a runbook:
-- superusers and roles holding BYPASSRLS skip row security entirely. [11]
REVOKE ALL ON documents FROM PUBLIC;
ALTER ROLE app_service NOBYPASSRLS;

§7 · Instrument

Find your own
crossover point.

Every recommendation above is a function of tenant count, tenant size, and your engine's per-partition overhead. Rather than assert where the line falls, here is the model — set it to your fleet and read the answer off it. The fixed-overhead slider is the term that decides everything; measure yours before trusting the default.

Corpus: 500,000 vectors. The model is stated in full in src/data/multitenancy.ts — disagree with it by changing the fixed-overhead slider, which is the term everything hinges on.

A2 · 100 partitions6.19 GiB
A1 · one shared partition1.54 GiB
vectorsHNSW graphper-pointfixed × partitions

75%

Memory saved by pooling

4.69 GiB

Replicated fixed overhead

1.43 GiB

Actual vector payload

76%

Of silo spent on splitting

Recommended configuration

Pooled bytes, siloed boundary

A1 · B1 · C3 · D1

A pooled index is 75% cheaper in resident memory at this shape, because 100 partitions × fixed overhead dominates a corpus of this size. Take the saving on storage and spend it on the enforcement axis — that is the trade this article argues for.

Per-partition fixed overhead (4.7 GiB) exceeds the vector payload itself (1.4 GiB). You would be spending more memory on the act of splitting than on the data being split.

At 5,000 vectors per tenant, every tenant sits behind a highly selective predicate over the shared graph. Declare the tenant payload index and hold a per-tenant Recall@k test, or §5 happens to you silently.

§8 · Field report

100+ environments,
one collection.

The production system this model came out of: a multi-tenant retrieval service for industrial fault-log search, where each tenant is a physically separate plant environment. It started as the textbook design and stopped being that after the memory attribution came back.

Baseline

100+ collections

One collection per environment

The obvious first design, and the one every tutorial endorses: each of 100+ industrial environments got its own Qdrant collection. Isolation was trivially explainable and the memory curve was linear in tenant count rather than in data — because most environments held a few thousand vectors and paid full per-collection overhead regardless.

Measurement

overhead > payload

Fixed overhead, not payload, was the bill

Attributing resident memory by component showed the corpus itself was a minority of the footprint. The majority was per-collection structure replicated 100+ times: segment metadata, identifier and version trackers, per-collection index scaffolding and optimiser working set. Splitting the corpus was costing more than the corpus.

Change

A2 → A1 · C1 → C3

Collapse to one collection, keep the boundary

All environments moved into a single collection partitioned on a tenant keyword payload index declared with `is_tenant=true`, so the engine co-locates each tenant's vectors and can estimate filter cardinality [6][7]. Storage isolation dropped from A2 to A1 — and enforcement was simultaneously raised to C3, because pooled bytes are only defensible behind a boundary stronger than the one you gave up.

Result

−80% memory

80% less resident memory, boundary unchanged

Eliminating replicated per-collection overhead cut resident memory by roughly 80% against the per-tenant baseline, at equal corpus size. No tenant's queries returned another tenant's points before or after — the difference is that afterwards, three independent mechanisms had to fail for that to become possible rather than one.

Second-order

150 ms → <5 ms p99

The retrieval path got faster for an unrelated reason

Consolidation forced a rewrite of the query path, which is where the intent-classification fast path was added: direct-lookup queries for exact alphanumeric codes bypass vector search entirely. That cut p99 retrieval latency from 150 ms to under 5 ms for that query class. Worth separating honestly — pooling did not cause the speedup, it caused the rewrite that contained it.

§9 · What it cost

Three things pooling took, in exchange.

A write-up without this section is marketing. None of these were visible at design time, and all three are operational — the axis that gets scored last.

01

Per-tenant restore became a project. In the collection-per-tenant design, restoring one environment was a snapshot restore. Pooled, it is a filtered export, a scoped delete, and a re-ingest — with a correctness argument attached. This is the single largest thing pooling cost, and it was not visible at design time.

02

Erasure needed proof, not a DELETE. A scoped delete removes points, but the vectors' contribution to graph structure and to any index statistics is not obviously gone until the segment is rewritten. Satisfying an Art. 17 erasure request [14] honestly meant forcing optimisation and verifying, not just issuing the delete and closing the ticket.

03

Recall for the smallest tenants needed its own test. Pooling put small tenants behind a selective predicate over a large shared graph — exactly the regime described in §5. It did not regress, because the payload index and planner thresholds were configured for it. It would have regressed silently if they hadn't been, and no latency or error-rate alarm would have fired.

§10 · Decision matrix

Six fleet shapes,
six configurations.

Read across: the shape of the fleet on the left, a coordinate on each of the four axes, and the reasoning that connects them. Nothing here is a rule — the arithmetic in §7 is the rule, and this is what it returns for six shapes I have actually had to build for.

ScenarioFleet shapeABCDReasoning
Self-serve B2B SaaS10k+ tenants, median a few thousand vectors, heavy long tail of dormant accountsA1B1C2–C3D1–D2Pool hard. Per-partition overhead would exceed the corpus itself. Spend the savings on enforcement and on offloading dormant tenants to object storage rather than RAM [9].
Enterprise B2B, mid-market50–500 tenants, hundreds of thousands of vectors each, contractual uptimeA2B2C2D2Past the crossover: per-tenant indexes now pay for themselves in isolation and per-tenant restore. Tier the fleet and promote tenants across the boundary automatically [15].
Regulated — health, finance, defenceAny size; auditor asks where the bytes physically areA3B3C3D2–D3Silo, and stop optimising. AWS names compliance pushback as a first-class reason pooling is unavailable regardless of the technical controls you can demonstrate [4]. Argue with the model, not with the auditor.
Consumer app, per-user memoryMillions of 'tenants', tens to hundreds of vectors eachA0–A1B1C1–C2D0–D1The user is not a tenant in the SaaS sense. Pool everything; the entire per-user corpus is smaller than one partition's fixed overhead. Enforcement is the only axis worth money here.
Internal platform, one company10–100 teams, trusted-ish, wildly uneven sizesA1B1–B2C1D1The real risk is a noisy neighbour, not exfiltration. Buy quotas and cost attribution first; buy storage isolation last. Most internal platforms get this backwards.
Hybrid: long tail + whales5,000 small tenants and 8 that are 40% of the corpusA1 + A2B2C2–C3D2The bridge model, and the most common real shape [5]. Pool the tail, pin the whales to dedicated shards, and build the promotion path before a whale forces you to build it live.

§11 · Failure modes

Seven ways
I have seen this go wrong.

Including two I shipped myself. Ordered roughly by how expensive they are to discover late.

01

Treating isolation as one slider

The failure this whole article is about. Teams pick 'pool' or 'silo' and inherit all four axes from that one word — usually inheriting a weak enforcement boundary from a storage decision that was made purely on cost.

02

Trusting a tenant id from the client

If the caller can name the tenant, the caller can name a different tenant. Scope belongs to the verified token, and there should be no parameter for it anywhere in the handler signature.

03

Filtering without a payload index on the tenant key

Without the index the planner cannot estimate cardinality, so it cannot choose between graph traversal and direct retrieval — and picks badly [7]. The symptom is inconsistent latency across tenants of different sizes, which is usually misdiagnosed as a scaling problem.

04

Benchmarking on the median tenant

Pooled systems fail at the tails. Benchmark the 5th and 95th percentile tenant by corpus size; the median is the one size that is guaranteed to look fine in every configuration.

05

Deferring per-tenant erasure until a customer asks

Erasure and export are the two operations with a legal clock attached [14]. In a pooled store they are engineering work. Building them under a thirty-day deadline is a bad time to discover your delete path doesn't reclaim graph structure.

06

Assuming per-partition cost is portable across engines

A collection, a namespace, and a shard are not the same unit. One engine caps you at 1,000 of them [6]; another is built to hand out a million [8][10]. An architecture that is obviously correct on one is obviously wrong on the other.

07

No cross-tenant probe in production

Every layer in §6 is a claim until something continuously tries to violate it with real credentials against real data and alerts when it succeeds. Staging does not count; staging has one tenant and it is you.

§12 · What I’d put on the whiteboard

01

Isolation is four decisions. Write down all four, or three of them will be made for you by the first one.

02

Pool the bytes if the arithmetic says so. Never pool the boundary.

03

Storage isolation is an efficiency decision. Enforcement isolation is a security decision. They share a word and nothing else.

04

A filter is a query-planner input, not a security control — even when it happens to be both.

05

The isolation model you can restore a single tenant from is the one you actually have.

References

Primary sources only — peer-reviewed papers, first-party engine documentation, and the regulation itself. Where a vendor is cited for a claim about their own engine, that is deliberate: they are the authority on their own per-partition costs, and not on anyone else’s.

  1. [1]

    Malkov, Y. A., & Yashunin, D. A.

    Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs

    IEEE TPAMI 42(4), 824–836, 2020 · arXiv:1603.09320

    Paper
  2. [2]

    Gollapudi, S., Karia, N., Sivashankar, V., Krishnaswamy, R., et al.

    Filtered-DiskANN: Graph Algorithms for Approximate Nearest Neighbor Search with Filters

    Proceedings of the ACM Web Conference 2023 (WWW '23)

    Paper
  3. [3]

    Patel, L., Kraft, P., Guestrin, C., & Zaharia, M.

    ACORN: Performant and Predicate-Agnostic Search Over Vector Embeddings and Structured Data

    Proc. ACM Manag. Data 2(3) — SIGMOD 2024 · arXiv:2403.04871

    Paper
  4. [4]

    Amazon Web Services

    Pool isolation — pros, cons, noisy neighbour, and compliance pushback

    AWS Well-Architected Framework, SaaS Lens

    Docs
  5. [5]

    Amazon Web Services

    The bridge model

    AWS Well-Architected Framework, SaaS Lens

    Docs
  6. [6]Docs
  7. [7]Engineering
  8. [8]

    Pinecone

    Implement multitenancy using namespaces

    Pinecone Documentation

    Docs
  9. [9]

    Weaviate

    Tenant states — active, inactive, offloaded

    Weaviate Documentation

    Docs
  10. [10]Engineering
  11. [11]

    The PostgreSQL Global Development Group

    Row Security Policies — CREATE POLICY, FORCE ROW LEVEL SECURITY, BYPASSRLS

    PostgreSQL Documentation, §5.9

    Docs
  12. [12]

    Pang, R., Cáceres, R., Burrows, M., Chen, Z., et al.

    Zanzibar: Google's Consistent, Global Authorization System

    USENIX Annual Technical Conference (ATC '19)

    Paper
  13. [13]Docs
  14. [14]

    European Parliament and Council

    Regulation (EU) 2016/679 (GDPR), Article 17 — Right to erasure ('right to be forgotten')

    Official Journal of the European Union

    Standard
  15. [15]Engineering