Multi-tenancy
Serving many customers from shared infrastructure while keeping their data, quotas and performance isolated. Isolation tier is a cost vs risk choice.
A multi-tenant system serves many customers (tenants) from the same software and often the same databases. Each customer must only ever see their own data, and one customer's heavy use shouldn't slow down the others. You can isolate tenants a lot (separate databases each, expensive) or a little (one shared database with a tenant id on every row, cheap but one bug away from a leak). Most products mix the two by customer size.
An apartment building: tenants share the walls, plumbing and lift, but each has their own locked door and their own water meter.
Isolating the database but not the caches. A semantic cache keyed only on the question text serves tenant A's answer, with tenant A's data in it, to tenant B asking the same question.
Three tiers: pooled (shared tables/index, tenant_id on every row, filtered on every query), bridge (shared cluster, separate namespace, schema or collection per tenant: Pinecone namespaces, Qdrant payload partitions with is_tenant, Postgres schema per tenant), silo (dedicated database or cluster per tenant). In pooled Postgres, enforce isolation with Row-Level Security rather than trusting every query: CREATE POLICY ... USING (tenant_id = current_setting('app.tenant_id')::uuid), set via SET LOCAL inside each transaction (critical with PgBouncer transaction pooling), and FORCE ROW LEVEL SECURITY because table owners bypass RLS otherwise. For vector search, filter by tenant inside the ANN search (pre-filter), not after top-k, or small tenants get empty results. Caches are a classic leak: every Semantic cache and prompt-cache key must include the tenant id. Isolation also means performance (Noisy neighbour), keys (per-tenant encryption for regulated customers), and data residency. Put tenant id in every log, trace and metric so you can answer 'what did tenant X see?'
Pooled Postgres with RLS (FORCE ROW LEVEL SECURITY, SET LOCAL app.tenant_id per transaction); vector store with tenant partitioning (Qdrant is_tenant payload index, Pinecone namespaces, or a tenant_id filter in pgvector with RLS); tenant id in every cache key, log, trace and metric; per-tenant rate limits; silo tier for regulated customers; cross-tenant test suite in CI.
A B2B legal-contracts SaaS has 1,800 tenants. 1,750 small firms are pooled in one Postgres cluster with RLS and a shared Qdrant collection partitioned by tenant_id (is_tenant: true). 45 mid-size firms get their own Qdrant collection. 5 enterprise banks are siloed: dedicated cluster in-region, customer-managed KMS keys, separate model API keys. A CI test runs every API endpoint as tenant A and asserts zero rows from tenant B.
I'd default to pooled with database-enforced isolation (Postgres RLS, tenant-partitioned vector collections, tenant id in every cache key) and offer silo as a priced tier for regulated customers. The follow-up: 'how do you prove isolation?' Automated cross-tenant tests on every endpoint, RLS so a missing WHERE clause fails safe, and tenant id on every trace for audits.
| Tier | Isolation | Cost/tenant | Use for |
|---|---|---|---|
| Pooled + RLS | row/filter | lowest | SMB, free tier |
| Bridge | namespace/collection | medium | mid-market |
| Silo | dedicated cluster | highest | regulated, residency |
Numbers worth knowing
- Pooled vs silo infra cost
- ~10–100× cheaper per small tenanta dedicated cluster has a fixed floor regardless of data size
- RLS query overhead
- low single-digit % on indexed tenant_idpolicy predicate uses an index; can be much worse if it calls a function per row
- Post-filter failure
- 0 results for a tenant with <1% of vectorstop-k=10 over the whole index, then filtered
How to decide
| When | Do this |
|---|---|
| Many small tenants | Pooled with RLS and filter-in-search. |
| Tenant needs residency, own keys or audit isolation | Silo, priced accordingly. |
| One tenant's data >10% of a shared index | Move it to its own collection (bridge). |
| Any shared cache | Tenant id in the key, always. |
Cost maths
Pooled: one 3-node vector cluster at ~$1,500/month serving 1,750 tenants ≈ $0.86/tenant/month. Silo: the smallest production-grade dedicated cluster (~$600–1,000/month) per tenant. Price silo as an enterprise add-on at a margin over that floor; a single cross-tenant leak in pooled mode costs far more in contracts and regulatory exposure, which is why the enforcement must be in the database.
Operating it
- One tenant >10% of queries or storage: promote to bridge or silo
- Enterprise prospects asking for residency or customer-managed keys
- Filtered ANN latency for small tenants degrading: add tenant-aware partitioning
- Cross-tenant test suite in CI and a nightly prod canary: alert on any cross-tenant row
- Queries executed without tenant context set (RLS denial count): alert >0 unexpected
- Per-tenant share of QPS and storage: alert when one tenant >10%
When it goes wrong
A tenant's support agent saw an answer that quoted another company's contract terms.
Customer report; traces showed the answer served from the semantic cache, originally generated for a different tenant.
Retrieval was tenant-filtered and Postgres had RLS, but the semantic cache key was hash(normalised_question), with no tenant id.
Tenant id and ACL hash added to every cache key, cache flushed, cross-tenant tests extended to caches, and incident disclosed to both tenants.
Related terms
Where it shows up
Sources
- Row Security Policies — PostgreSQL, 2025docs
- Implement multitenancy — Pinecone, 2025docs
- OWASP Top 10 for LLM Applications 2025 — OWASP, 2025spec
Check yourself
Three questions. Every option is explained once you answer.