Multi-Tenancy Isolation Models
Multi-tenant SaaS architectures balance tenant data isolation against infrastructure cost:
- Silo Model (Database-per-Tenant): Each tenant gets a dedicated database instance. Provides maximum isolation and security compliance; highest operational and infrastructure cost.
- Bridge Model (Schema-per-Tenant): Tenants share a database cluster but use separate PostgreSQL schemas.
- Pool Model (Shared Database & Tables): All tenants share the same tables, with every row containing a
tenant_idcolumn. Most cost-effective; requires strict Row-Level Security (RLS) to prevent cross-tenant data leaks.
The Noisy Neighbor Problem
A single large tenant executing heavy batch queries can starve CPU and IOPS, degrading performance for all other tenants. Multi-tenant systems enforce per-tenant rate limits, resource quotas, and tenant-aware database connection pools.
Isolation models compared
| Model | Isolation | Cost per tenant | Operations at 10,000 tenants | Typical fit |
|---|---|---|---|---|
| Silo (database or account per tenant) | Strongest; separate blast radius and encryption keys | High | 10,000 databases to migrate, monitor and back up | Enterprise, regulated or data-residency customers |
| Bridge (schema per tenant) | Medium; shared engine and resources | Medium | Schema migrations times 10,000; catalogue bloat in PostgreSQL | Mid-size B2B with a moderate number of tenants |
| Pool (shared tables with tenant_id) | Logical only; needs RLS and careful keys | Lowest | One schema; shard by tenant_id | Self-serve or long-tail customers |
| Cell-based (pool within cells, many cells) | Blast radius limited to one cell | Low to medium | Tens of cells, each a full stack | Large SaaS such as Slack or Salesforce, and AWS services |
Worked example: tiered tenancy for a B2B analytics product
- 9,800 free and Pro tenants average 2 GB each, about 20 TB in total. They are pooled across 8 Postgres shards (for example with Citus distributing on
tenant_id), roughly 2.5 TB per shard. - 200 enterprise tenants average 300 GB each. Anyone above 1 TB, or with a residency requirement, gets a silo database in the region they need.
- Per-tenant limits in the pool: 50 queries per second, a 10 s statement timeout, and at most 5 concurrent heavy queries, enforced at the API gateway and by a tenant-aware connection pool.
- Moving a growing tenant from pool to silo uses the same expand, copy, dual-write, cut-over process as any database migration, keyed on
tenant_id. Build this tooling early; you will need it.
Enforcing isolation in the pool
In PostgreSQL, enable RLS with a policy such as USING (tenant_id = current_setting('app.tenant_id')::uuid), and set app.tenant_id at the start of every transaction from the authenticated token. Keep in mind:
- Table owners and superusers bypass RLS unless you use
FORCE ROW LEVEL SECURITY. Run the application as a role without those privileges. - With transaction-mode connection pooling (PgBouncer), use
SET LOCALinside the transaction, so a session setting cannot leak to the next tenant who gets that connection. - Put
tenant_idfirst in composite indexes and primary keys, so queries for one tenant stay on a narrow index range and the table can be sharded by tenant later.
Noisy neighbour controls
Tag every metric and log line with tenant_id, keeping it in logs and exemplars where cardinality is too high for metric labels. Without per-tenant usage data you cannot find the noisy neighbour, charge fairly, or decide who to move.