Multi-Tenancy
Date: 2026-08-17
One running system serving many customers who must never see each other’s data. The architecture question is where the isolation boundary sits, and the answer is a straight trade between cost per tenant and the blast radius of a single mistake.
Multi-tenancy is a single deployment of software serving multiple customers (tenants), with each tenant’s data and configuration logically isolated from the others.
The three models
SHARED EVERYTHING SHARED APP, DB PER TENANT SILO
┌──────────────┐ ┌──────────────┐ ┌────┐ ┌────┐ ┌────┐
│ app │ │ app │ │app │ │app │ │app │
└──────┬───────┘ └──┬───┬───┬───┘ │ + │ │ + │ │ + │
│ │ │ │ │db │ │db │ │db │
┌──────┴───────┐ ┌──┴┐┌─┴─┐┌┴──┐ └────┘ └────┘ └────┘
│ one database│ │db1││db2││db3│ A B C
│ tenant_id │ └───┘└───┘└───┘
│ on every row│
└──────────────┘
cheapest per tenant middle most expensive
one bad WHERE = breach isolation by construction total isolation
noisy neighbours per-tenant backup/restore per-tenant everything
one migration N migrations N of every operation
The middle column is where most B2B software ends up, and the left column is where most of it starts.
Shared everything, done safely
The tenant_id column model is fine — the risk is that one missing WHERE tenant_id = ? is a data breach, and there are hundreds of queries.
Never rely on developers remembering. Enforce it below the application:
-- Postgres row-level security: the database refuses to return other tenants' rows
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::uuid);The application sets app.current_tenant once per request, from the authenticated session — never from a URL, a header or a form field. The tenant identifier must come from something the user cannot alter, or you’ve built a breach with extra steps.
Belt and braces worth having: a repository layer that cannot construct an unscoped query, and a test that runs every query as tenant A and asserts zero rows belonging to tenant B.
Noisy neighbours
Shared compute means one tenant’s behaviour is another tenant’s latency — a bulk import, a runaway report, a bot crawling their catalogue.
- Per-tenant Rate Limiting, not just global
- Query timeouts and row limits per request
- Separate the batch path from the interactive path — a queue and its own workers, so a big import can’t starve page loads — Message Queues
- Watch the tenant distribution, not the average. p99 latency hides which tenant owns it; per-tenant p99 tells you who to call — Percentiles in Performance
The operations that get harder
| Operation | Shared | Isolated |
|---|---|---|
| Deploy a schema change | Once, atomically | N times, and now some tenants are on the old schema |
| Restore one tenant’s data | Hard — extracting one tenant from a shared backup | Trivial — restore their database |
| Delete a tenant (GDPR, offboarding) | A query per table, and you’ll miss one | Drop the database |
| Per-tenant data residency | Impossible — one database is in one region | Natural |
| Cost attribution | Estimated | Measured |
Restore and delete are the two people underestimate. “Customer B needs yesterday’s data back, customers A and C must not be affected” has no clean answer in a shared database, and it will be asked.
Data residency is the one that forces the decision. UK GDPR — the UK’s retained version of the General Data Protection Regulation — doesn’t require local storage, but customer contracts frequently do, and a shared database cannot satisfy “our data stays in the EU” for one tenant — Data Residency, UK GDPR and PECR for Analytics.
Tenant-aware everything else
The boundary leaks past the database, and these are the ones missed:
- Caches. The tenant must be in the cache key. A shared key is a cross-tenant data leak served at CDN speed — Caching Strategies
- Search indexes. Same problem, usually solved with an index per tenant or a mandatory filter
- Background jobs. The job payload carries the tenant; the worker must set the context before doing anything
- Logs and error reports. Useless without the tenant, and a data-protection problem if they carry customer records — PII in Analytics, Error Tracking
- File storage. Path prefixes are not access control. Sign per-tenant URLs
- Feature flags. Tenant is an obvious targeting dimension, and a flag evaluated without it rolls out to everyone — Feature Flags
Choosing
- Self-serve, many small tenants, low price point → shared everything with row-level security. Anything else is unaffordable
- Enterprise, few large tenants, contractual isolation → database per tenant, shared application
- Regulated, or genuinely different per-customer versions → silo, and accept that you now run N systems
- Mixed → tiered, which is common and honest: shared by default, dedicated on the enterprise plan. Design for the migration path between tiers on day one, because retrofitting it means moving live data
Where it interacts
- Database Migrations — per-tenant databases turn one migration into a fleet operation that must be resumable and must tolerate tenants mid-upgrade
- Authorisation Models — tenancy is the outermost authorisation check, and it’s separate from what a user may do within their tenant
- Service Level Objectives — one tenant’s outage is not “99.9% availability” if it’s the tenant that matters; measure per tenant