Multi-Tenancy
One deployment serving many customers, where the worst possible bug is showing one of them another one's data.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What changes in a backend when one deployment serves many customers who must never see each other?
A B2B analytics product. Acme and Globex both log in to the same application. Each sees only their own dashboards, users and billing. Acme runs a report that scans ten million rows; Globex must not notice.
Add a tenant_id column to every table, filter by it in the places that matter, and give each customer a subdomain. The application is otherwise unchanged.
"The places that matter" is every query, including the ones written next year, the report builder, the admin tool and the migration script. One unfiltered query is a cross-tenant data leak.
- "The places that matter" is every query, including the ones written next year, the report builder, the admin tool and the migration script. One unfiltered query is a cross-tenant data leak.
- Acme's ten-million-row report saturates the connection pool and every Globex request queues behind it. Isolation of *data* says nothing about isolation of *resources* (Connection Pool Exhaustion).
- A background job has no session, so "the current tenant" is undefined inside it — and the job is where bulk data lives (Background Jobs).
- Caches, search indexes, uploaded files, exported reports and log lines all acquire tenant-crossing potential, and none of them are covered by a
WHEREclause (Tenant Isolation). - A large customer arrives and asks where their data is stored, whether it can be in the EU, and whether they can have their own encryption key. A single shared schema has one answer to all three.
- Per-tenant configuration, feature flags, limits and billing all need a home, and it is not the code (Feature Flags: Rollout, Kill Switches and Debt).
What is actually happening
- Multi-tenancy is one running system serving mutually distrusting customers. Everything downstream follows from that distrust: the tenant is a dimension on the data, on the authorization decision, on resource limits, on configuration and on observability.
- There are three common data strategies, and they trade isolation against operational cost. Shared schema with a tenant column: one database, one set of tables,
tenant_ideverywhere. Schema per tenant: one database, a namespace per tenant. Database (or cluster) per tenant: full separation. - Shared schema is cheapest to operate and has the weakest isolation: a forgotten predicate is a leak, and there is no infrastructure-level backstop unless you add row-level security (Where the Check Belongs).
- Schema-per-tenant makes accidental cross-tenant queries much harder (a query names one schema) and makes schema migrations a loop over N schemas, with partial-failure states in the middle (Schema Migrations from the Application Side).
- Database-per-tenant gives real blast-radius isolation, per-tenant backup, restore and residency — and multiplies connection pools, migration runs, monitoring targets and cost. It is usually reached from the top of the customer list downward, not chosen for everyone.
- Authorization becomes two-dimensional: which tenant (isolation) and what may this user do inside it (roles and object rules). They are separate checks with separate failure modes, and the tenant one is not optional (Role-Based Access Control).
- Resource fairness is the other half. Without per-tenant limits, one customer's workload is every customer's latency — the noisy-neighbour problem (Rate Limiting).
Three strategies, and what each one buys
These are not levels of maturity. They are different trade-offs between isolation strength and operational cost, and the right one is decided by requirements you can name — residency, contractual separation, per-tenant keys, blast radius, noisy neighbours — not by expected growth.
What isolation does a customer contract, a regulator or your failure model actually require?
when Many tenants, similar size, no residency or physical-separation requirement. The default for most SaaS.
cost Isolation lives entirely in application code and in every future query. One missing predicate is a breach.
when Same as above, on an engine that supports it, when you want a backstop against application bugs.
cost Per-connection session state must be set and reset correctly under pooling; policy lives where app engineers do not look (Connection Pools).
when Hundreds of tenants, some need per-tenant backup or restore, and cross-tenant queries should be structurally hard.
cost Migrations become a loop with partial-failure states; connection and catalogue overhead grows with tenant count.
when Contractual or regulatory separation, per-tenant encryption keys, residency, or a very small number of very large tenants.
cost Every operational task multiplies: migrations, backups, monitoring, pools, cost. Usually only viable for tens or low hundreds.
when A long tail of small tenants plus a few enterprise accounts with their own requirements.
cost Two code paths and a tenant→location lookup that every component must consult correctly.
The tenant is a dimension on everything, not just rows
The tenant_id column is where teams start and where they stop. Every other place a tenant's data comes to rest needs the same dimension, and each one has leaked in a real product: cache keys, object storage prefixes, search indexes, exported files, queue payloads, metric labels, log lines and error reports.
- Cache keys — a key without the tenant serves one tenant's value to another.
- Object storage — prefix keys with the tenant; never let a client choose the key (Presigned URLs).
- Search indexes — either an index per tenant, or a tenant filter applied by the code that builds every query.
- Queue payloads — the job has no session; the tenant must be in the message and re-checked by the worker.
- Exports and reports — generated files are tenant data with a filename an attacker may be able to guess.
- Telemetry — tenant-labelled, cardinality-bounded, and scrubbed of tenant *content* (Secrets in Logs).
Noisy neighbours
Data isolation and resource isolation are separate properties, and most teams only build the first. Shared infrastructure means shared queues, shared pools, shared workers and shared rate limits — and one tenant can consume all of any of them without breaching anything.
The general answer is per-tenant bounds on anything finite, plus a way to see consumption per tenant. Fair-share scheduling of work matters more than raw capacity: a global limit that one tenant fills is still an outage for everyone else.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| One tenant runs a huge report | p99 rises for all tenants; pool wait time climbs | Shared connection pool with no per-tenant cap | Bound concurrent expensive queries per tenant; move long reports to a job queue with its own workers (Bulkheads). |
| One tenant enqueues 500k jobs | Every other tenant's jobs are hours late | A single FIFO queue shared by all tenants | Per-tenant queues or fair-share dequeuing; cap in-flight jobs per tenant (Queue Backlog). |
| One tenant's integration retries aggressively | A shared third-party rate limit is exhausted for everyone | Global quota consumed by one tenant | Per-tenant budgets against shared external quotas (Rate Limiting). |
| One tenant has 100x the data | Queries that were point lookups become scans for that tenant only | Indexes and query plans tuned for the median tenant | Index on (tenant_id, ...) so selectivity holds; consider dedicated infrastructure for the outlier (Composite Indexes and the Leftmost-Prefix Rule). |
| A tenant is deleted | Orphaned files, cache entries and index documents remain | Deletion implemented as a database concern only | A tenant lifecycle that covers every store the tenant dimension reaches (Storage Lifecycle: Hot, Warm, Archive, Delete). |
How to build it
Most important first.
- Derive the tenant from the authenticated principal, never from a header, path segment, subdomain or body field the client controls. This is the single rule that everything else rests on (Tenant Isolation).
- Make tenant scoping structural rather than remembered: a repository layer that requires a tenant argument, a query builder that injects the predicate, or row-level security in the database. A convention that "everyone filters by tenant" is not a control (The Repository Layer).
- Choose the data strategy by the requirement that actually forces it — residency, per-tenant encryption, contractual isolation, or noisy neighbours — and keep the mapping from tenant to location in one place so it can change per tenant later.
- Put the tenant id on every log line, metric label and trace span, so "is this slow for everyone or for Acme" is answerable (Correlation Ids That Survive Every Hop).
- Apply per-tenant limits: request rate, concurrent expensive operations, job concurrency, storage. A global limit does not stop one tenant consuming all of it (Resource Limits).
- Make background jobs carry the tenant explicitly in their payload and scope their queries by it, with the same enforcement as request paths.
- Design the per-tenant configuration path early — limits, flags, plan features — so that "Acme needs a higher export cap" is a data change rather than a deploy.
What can go wrong
- A single query missing the tenant predicate, in a report, an export, an admin screen or an aggregate. One line, unlimited exposure.
- A cache key without the tenant, so the second tenant to request a key receives the first tenant's value (Cache-Aside).
- A search index shared across tenants without a tenant filter applied at query time (Keeping a Search Index in Sync).
- Migrations that succeed for 900 tenants and fail for 100, leaving the application running against two different schemas (Expand and Contract Migrations).
- One tenant's heavy usage exhausting a shared connection pool, worker pool or rate limit budget, degrading everyone (Unbounded Concurrency).
- Sequential ids that leak business information across tenants — an invoice number that reveals your total customer count.
- A "support impersonation" feature that switches tenant context without logging, so cross-tenant access by staff is untraceable (Audit Logs for Privileged Actions).
- Per-tenant costs invisible in aggregate metrics, so the tenant costing more than they pay is discovered at renewal (Cost per Request: The Other Performance Metric).
- A tenant's plan or limits changing while requests are in flight, so two concurrent requests are evaluated against different limits.
- Provisioning a new tenant concurrently with a schema migration, producing a tenant on the old schema (Schema Migrations from the Application Side).
- Impersonation sessions overlapping with the real user's own session in a shared context store, so the effective principal depends on ordering (Request Context Propagation).
- If tenant scoping is missing on any read path, an attacker gets other companies' data — the most serious breach class for a B2B product, because it is a contractual and regulatory failure as well as a technical one.
- If the tenant is taken from a request field, an attacker gets *every* tenant by iterating that field. A
X-Tenant-Idheader trusted by the backend is a complete authorization bypass in one line (The Trust Boundary). - If tenant is derived from a subdomain without checking that the principal belongs to it, an attacker with an account on one tenant gets access to another by visiting its subdomain while holding their own session cookie.
- If caches, indexes or file paths are not tenant-scoped, an attacker gets a cross-tenant read without any request-level vulnerability at all — the leak happens in infrastructure the application code never inspects.
- If staff impersonation is unlogged or unlimited, an insider gets unauditable access to every customer. The technical control is scoped, time-limited, logged impersonation with the real actor recorded alongside the impersonated principal.
- If per-tenant limits are absent, one tenant — or an attacker with one account — gets a denial-of-service lever against every other customer (Rate Limiting).
- "Multi-tenancy is just a
tenant_idcolumn." The column is the easy part. The isolation of caches, files, jobs, indexes, logs and resources is the work (Tenant Isolation). - "Separate databases mean we do not need authorization." They mean cross-tenant leaks are unlikely. Inside a tenant, users still need roles and object-level checks (Object-Level Authorization).
- "Subdomains isolate tenants." A subdomain is a routing hint. Unless the principal is checked against it, it is a suggestion from the client.
- "We will add multi-tenancy later." Retrofitting a tenant dimension onto every table, cache key, file path and index is one of the most expensive migrations a product can undertake.
- "Noisy neighbours are a scaling problem." They are an isolation problem that appears as a scaling problem, and adding capacity does not fix the sharing.
Operating it
- Label metrics with tenant id, but bound the cardinality: label the top N tenants explicitly and bucket the rest, or you will melt the metrics backend (Cardinality: The Label That Took Down Monitoring).
- Alert on any query executed without a tenant predicate. Some teams enforce this in a database proxy or a test that inspects generated SQL; both are more reliable than review.
- Track per-tenant latency, error rate, query time and job backlog. Aggregate percentiles hide a single customer having a terrible day (Percentiles: Which One, and How Many Users Is That?).
- Log every impersonation session with the real actor, target tenant, reason and duration.
- Track per-tenant resource consumption alongside plan, so noisy neighbours and unprofitable accounts are visible before they are incidents (Cost Engineering).
- At 10x tenants, shared schema is usually still right, and the operational questions shift to per-tenant limits and per-tenant visibility rather than data layout.
- At 100x, the tail matters: a few very large tenants alongside thousands of tiny ones. The common answer is a hybrid — shared infrastructure for the many, dedicated databases or clusters for the few who justify it — with a tenant→location lookup that the application consults.
- Migration cost scales with the number of schemas or databases, not with data volume. At a thousand databases, migrations become a fleet operation with orchestration, retries and partial-state handling.
- Very large tenants eventually need per-tenant sharding *inside* their own data, which is a different problem again (Partitioning and Sharding).
- Shared schema is cheapest and puts the entire isolation guarantee in application code. Every new query is a chance to break it.
- Database-per-tenant gives the strongest isolation and multiplies every operational task: migrations, backups, monitoring, connection pools, cost. It is genuinely more secure and genuinely more expensive to run.
- Row-level security is a strong backstop for shared schema and requires correct per-connection session state, which interacts badly with pooling if done carelessly (Connection Pools).
- Per-tenant limits protect the many and frustrate the one legitimately-heavy customer, who will ask for an exemption. Design the exemption path deliberately rather than by ad-hoc override.
- Tenant-labelled telemetry is enormously useful and expensive at high cardinality.
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- GENERALThe tenant-as-a-dimension framing applies to any product serving mutually distrusting customers from one deployment.
- DATABASE-SPECIFICThe three strategies have different feasibility per engine: Postgres schemas are cheap and its row-level security makes shared-schema safer; MySQL treats a "schema" as a database, so schema-per-tenant is database-per-tenant with different words, and it has no row-level security. Managed database services also cap how many databases one instance can hold, which bounds database-per-tenant well before your customer count does.
- SCALE-SPECIFICBelow roughly a few hundred tenants, shared schema with disciplined scoping is usually the right call and the operational alternatives are pure overhead. The hybrid model becomes worth its complexity when a small number of customers are much larger than the rest, or when a contract requires physical separation.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.
- — Distributed Systems — tenant placement, rebalancing and per-tenant failure domains are the same problems as shard placement, seen through a commercial lens.