System designDeep dive13 min
Multi-tenancy
One deployment serving thousands of customers is what makes software as a service cheap to run. It also means every query, cache, queue and admin script is one mistake away from showing a customer someone else's data, letting one customer slow everyone down, or breaking hundreds of customers at once. Multi-tenancy is the set of decisions that keep those three failures apart.
1 · The problem
Sharing is cheap until one tenant sees, slows or breaks another
A help-desk product serves 6,000 companies. A dental clinic with two agents and a national retailer with 900 agents use the same servers, the same database and the same search cluster. Each company is a tenant: the unit whose data, users and settings belong together and must stay apart from every other's. For a business product, a tenant is usually one customer organization, though a large customer may want several.
Sharing is what keeps the price low. With one set of infrastructure there is one thing to deploy, patch and monitor, and capacity follows the total load instead of being reserved per customer. The price of sharing is three new ways to fail:
Failure | What happens | Help-desk example |
|---|---|---|
Leak | One tenant sees another's data | The clinic's search returns the retailer's tickets |
Noisy neighbor | One tenant's load slows the others | The retailer imports 2 million old tickets at 10 a.m. |
Shared blast radius | One fault or bad command hits many tenants | A cleanup script deletes the wrong accounts |
Each section below takes one of these in turn. The first decision, though, is how much to share at all, because every answer changes how hard each failure is to prevent.
2 · The models
Silo, pool and bridge are points on one spectrum, chosen part by part
At one end, each tenant gets a complete copy of the system: its own servers, database and network. This is the silo model. At the other end, everything is shared and each row carries a tenant id: the pool model. Most real systems are a bridge between them, sharing some layers and separating others, and the choice can differ for each component and each customer tier:
Aspect | Silo | Pool | Bridge |
|---|---|---|---|
Shared | Nothing | Everything | Some layers, such as web servers |
Leak risk | Low; the boundary is the infrastructure | Highest; the boundary is code and queries | Per layer |
Noisy neighbors | None | Must be managed | Only in shared layers |
Cost | Roughly linear in tenants | Follows total load | Between the two |
Operations | Many copies to deploy and upgrade | One deployment, one upgrade | A few copies plus the pool |
Silos are expensive: if one tenant needs a given amount of infrastructure, 100 tenants probably need 100 times as much, and every change has to be rolled out to each copy. Silos earn their cost when customers have strict regulatory isolation requirements or won't accept sharing, and a silo also limits how far one outage or bad release spreads.
A common split for the help desk: the web tier and application servers are pooled for everyone, while the data tier is pooled for small customers and dedicated for the few that pay for it. The same pattern shows up as tiers, with the premium tier running a dedicated copy of exactly the same version as the pool so that it doesn't become a one-off to maintain.
One shape to avoid inside a shared database is a separate set of tables for each tenant. It doesn't scale to large numbers of tenants and becomes hard to query, manage and upgrade; use shared tables with a tenant id column, or separate databases. Custom columns for one tenant's needs are the same trap, and belong in a table for per-tenant custom data.
3 · Isolation
In a pool, the tenant must come from a verified identity and be enforced below the application
The first leak is usually one forgotten filter. The ticket search runs SELECT ... WHERE subject LIKE ... and someone forgot AND tenant_id = ... in one code path. In pooled systems, sharing increases the chance of cross-tenant access, which is a reason for more diligence, never for relaxing isolation.
Two rules close most of the gaps. First, the tenant comes from something the server has verified, such as a claim in a signed token, not from a header or URL the client can change. Even when the request arrives on the tenant's own domain, check that the caller is authorized for that tenant. Second, the tenant is applied by a layer every query passes through, so one forgotten clause can't leak:
The tenant is taken from the verified token at the edge and passed down, never chosen by the client. The database applies it again, so a query that forgets the filter returns nothing extra.
The database layer can be row-level security, which PostgreSQL and Azure SQL Database both offer. In PostgreSQL, a policy adds a condition to every query on the table, and once row security is enabled, a table with no policy shows no rows at all:
One policy for every ticket query
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
ALTER TABLE tickets FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON tickets
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- per request, inside its transaction:
SET LOCAL app.current_tenant = '0b6f...';It has sharp edges:
- Owners and superusers bypass it. Superusers and roles with BYPASSRLS always skip policies, and so does the table's owner unless the table is set to FORCE ROW LEVEL SECURITY. An application that connects as the owner has no protection at all.
- Constraint checks bypass it. Unique and foreign-key checks see every row, which can reveal that a value exists in another tenant.
- It's harder to build and test. Many multitenant systems skip row-level security because the tenant has to be carried into every query.
The tenant setting itself can leak through a connection pool. A plain SET lasts for the whole database session, and pooled connections outlive requests:
A code path that skips setting the tenant inherits the previous request's. With SET LOCAL inside a transaction, the setting ends with the transaction, committed or not, so a path that skips it finds no tenant set instead of the previous request's.
The same discipline applies to everything else that holds tenant data: cache keys, search indexes, file paths in object storage, queue messages and logs all carry the tenant id, and every read checks it. A cache key of ticket:4411 instead of clinic-17:ticket:4411 is the same leak as a missing WHERE clause.
4 · Noisy neighbors
One tenant's peak becomes everyone's slowdown unless each tenant has limits
At 10 a.m. the retailer starts importing 2 million old tickets. The import saturates the shared database, and the clinic's agents wait 8 seconds for every page. This noisy neighbor problem has two forms: one tenant using a disproportionate share, and many small tenants whose peaks coincide, so that each uses a little but together they exceed capacity.
No shared resource can rule it out entirely; the work is making it unlikely and contained. The mitigations, in roughly the order to reach for them:
Mitigation | In the help desk |
|---|---|
Per-tenant limits and quotas | Each tenant gets its own request rate and concurrent-import limit (Rate limiting algorithms) |
Restrict heavy operations | Searches capped in rows and time; imports and exports run as background jobs |
Move work off-peak | Bulk imports queue and run at night unless the tenant pays for priority |
Measure per tenant | Dashboards and alerts per tenant, not just totals, so the noisy one is visible |
Rebalance or isolate | Move the retailer to its own database, or split tenants across copies of the system |
Per-tenant measurement does double duty. Shared services rarely report usage by tenant, so the product has to measure it, both to price plans and to see which tenants cost more to serve than they pay. A tenant that uses little storage but runs heavy reports may cost far more than its data volume suggests.
Isolating the shared resources themselves, with separate pools per tenant class, complete copies of the system called cells, or shuffle sharding, is covered in Bulkheads. The point here is that a tenant id on every request is what makes any of those limits possible.
5 · Placement
A tenant catalog decides where each tenant lives, so tenants can move
As the help desk grows, one database is no longer enough, and some tenants need their own. The answer is to shard by tenant (Sharding & partitioning): each shard holds many small tenants, or one large one, and a tenant catalog maps every tenant to the shard or deployment that holds its data. Every request looks up its tenant first, then goes to the right place:
Because every request consults the catalog, it has to be fast and highly available, usually behind a cache. Changing one entry in it is how a tenant moves.
Separate copies of a system are called deployment stamps, and they're good practice for multitenant systems, even when each stamp is itself pooled or sharded. A stamp per geography also serves tenants whose data must stay in one region (Privacy, residency & retention).
Tenants have to move: a small tenant grows, a customer upgrades to a dedicated tier, a stamp gets too full. Build tools that make moving repeatable and safe rather than doing it by hand. With a catalog, a move is a data migration followed by one catalog change:
- Copy
Copy the tenant's rows to the new database while the tenant keeps working on the old one.
- Catch up
Apply the changes made since the copy started, from the old database's change stream (Change data capture).
- Switch
Pause the tenant's writes for seconds, apply the last changes, and point the catalog at the new location.
- Verify, then delete
Compare counts and checksums, keep the old copy until the move is trusted, and only then delete it.
6 · Operations
Every operation needs a tenant scope, or one mistake reaches every customer
On April 5, 2022, Atlassian ran a script to remove a retired app from customer sites. The team that requested the deletion supplied the IDs of the customers' entire sites instead of the app's IDs, and the deletion API accepted either kind of ID without asking which was meant. Between 07:38 and 08:01 UTC the script deleted 883 sites belonging to 775 customers. Some customers were without their products for up to 14 days.
Atlassian's review explains why the restore took so long. Each site's data was spread across many services and stores, and while any single store could be restored to an earlier point in time, there was no automated way to restore a large set of sites with all their connected products. The first change it listed was universal soft deletes: no customer data is removed without first passing through a reversible delete.
The lessons carry to any multi-tenant system. Every administrative action, backup and restore needs to work at the level of one tenant, and a bulk action across tenants needs the same staged rollout as code:
Operation | Single-tenant question | Help-desk answer |
|---|---|---|
Restore | Can one tenant be restored without rolling back the others? | Restore the shard to a side copy, then copy that tenant's rows back |
Delete or offboard | Is it reversible, and is everything found? | Soft delete, a grace period, then removal from every store, keyed by tenant id |
Bulk scripts | What kind of ID is this, and how many tenants? | Typed IDs, a dry run, a small first batch, then the rest |
Releases | Who sees a bad release first? | Internal tenants, then a small share of the pool, then everyone |
Restoring one tenant from a shared database is the hard case. It may mean restoring the whole database to a separate resource and copying that tenant's data back; dedicated databases make it simpler, though restoring a shard still touches every tenant on it. Plan and test the single-tenant restore before a customer asks for it.
7 · The checklist
Decide what to share, then make the tenant explicit everywhere
For the help desk, and most software sold to many organizations:
Decision | Help-desk choice |
|---|---|
What to share | Pooled web and application servers; pooled shards for most tenants; dedicated databases for those who need and pay for them |
Where the tenant comes from | A claim in a verified token, enforced again by row-level policies |
Where the tenant id goes | Rows, cache keys, search documents, object paths, messages, logs and metrics |
Fair sharing | Per-tenant limits, background imports, per-tenant dashboards |
Placement | A tenant catalog, so tenants can move between shards, tiers and regions |
Operations | Restore, delete, migrate, bulk scripts and releases all scoped to tenants |
Testing | Automated tests that sign in as one tenant and try to read another's data |
Whatever the model, test that one tenant's data isn't leaked to another, and that the noisy neighbor effects you accept are the ones you expected.
Check yourself
4 questions
Takeaways
Remember this
- A multi-tenant system must prevent three failures: one tenant seeing another's data, slowing it down, or being hit by an operation meant for someone else.
- Silo, pool and bridge are a spectrum; choose per component and per customer tier, trading cost against isolation.
- Take the tenant from verified identity, enforce it below the application, and put it in every key, cache entry, message and log.
- Limit and measure usage per tenant, so a noisy neighbor is both throttled and visible.
- A tenant catalog and tenant-scoped operations make it possible to move, restore and delete one tenant without touching the rest.