Tags: web-dev concept

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

OperationSharedIsolated
Deploy a schema changeOnce, atomicallyN times, and now some tenants are on the old schema
Restore one tenant’s dataHard — extracting one tenant from a shared backupTrivial — restore their database
Delete a tenant (GDPR, offboarding)A query per table, and you’ll miss oneDrop the database
Per-tenant data residencyImpossible — one database is in one regionNatural
Cost attributionEstimatedMeasured

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