Module P-8·32 min read

Database-per-tenant vs shared schema vs schema-per-tenant, what each costs in migrations and blast radius, and how to contain a whale account.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module P-8 — Multi-Tenancy: Silo, Pool, Bridge, and Noisy Neighbours

What this module covers: The three isolation models — silo (database or cluster per tenant), pool (shared tables with a tenant_id column), and bridge (schema per tenant) — compared on migrations, backup granularity, blast radius, per-tenant cost, and onboarding speed. Then the specific way each one hurts: silo's migration queue and cost floor, bridge's catalogue and tooling collapse, pool's missing WHERE clause, and row-level security as the structural fix for the last of those. Then the operational core of the topic — noisy neighbours, and the four levers that contain them. Finally, how to promote one large tenant out of the pool and into a silo without downtime.


Every Serious System Eventually Serves Someone Else's Customers

Up to this point the course has treated your data as your data. One logical dataset, partitioned for size (Module P-6) and placed for locality (Module P-7). Multi-tenancy breaks that assumption in a specific way: the dataset now has a hard internal boundary that most of your queries must respect, and the traffic arriving at it is no longer one aggregate load curve but hundreds or thousands of independent ones, each with its own peak, its own growth rate, and its own capacity to ruin everyone else's afternoon.

This module treats multi-tenancy as a partitioning and capacity problem. Where does the tenant boundary live in the storage layout, and how do you stop one tenant consuming a shared resource that everyone depends on. The enforcement question — proving that a request genuinely belongs to the tenant it claims, and the confused-deputy risk when an internal service holds broader privileges than the caller who invoked it — is Module A-13's subject, and it is a different problem with different tools. A perfect isolation model with a broken authorisation check leaks data anyway.

This is the first step of a through-line that runs the length of the course: P-8 chooses the isolation model → Module P-18 puts per-tenant limits on it → Module A-13 enforces the boundary and closes the confused-deputy hole → Module O-8 turns it into a cost-per-tenant number your finance team can read. The choice you make here determines how expensive each of the later three is.


Silo, Pool, and Bridge Are Three Places to Put the Same Boundary

All three models answer one question — at what level of the storage hierarchy do tenants stop sharing?

Silo. Each tenant gets its own database, or its own cluster. Nothing is shared below the application tier. The tenant boundary is an infrastructure boundary.

Pool. All tenants share the same tables. Every tenant-scoped row carries a tenant_id column, and every query filters on it. The tenant boundary is a predicate.

Bridge. All tenants share one database instance, but each gets its own schema (in Postgres terms, its own namespace with its own copy of every table). The tenant boundary is a namespace.

Analogy: silo is a detached house per tenant — own plumbing, own meter, own front door, and someone has to maintain every one of them. Pool is an open-plan floor with assigned desks: cheap, dense, and the only thing stopping you reading your neighbour's screen is that you're supposed to look at your own. Bridge is a block of flats — separate front doors, shared water main, shared lift, and a building manager whose job gets harder with every flat added.

Compared on the properties that actually decide the argument:

PropertySilo (DB/cluster per tenant)Bridge (schema per tenant)Pool (shared tables + tenant_id)
Schema migrationsN migrations, N failure modes, hours-to-days for large N (Module O-4)N migrations inside one transaction-per-schema loop; catalogue locks biteOne migration, one plan, one rollback
Backup/restore granularityNative — restore one tenant to a point in time with no custom toolingPer-schema dump possible, but a full restore is all-or-nothingNone by default; per-tenant restore is a custom export/import job
Blast radius of a bad deploy or bad queryOne tenantAll tenants on that instanceAll tenants
Noisy-neighbour exposureLow (per-tenant resources)High (shared buffers, WAL, connections, autovacuum)High (shared everything, plus shared indexes)
Per-tenant cost floorHigh — a mostly-idle instance still costs its baselineLowLowest; marginal cost per small tenant approaches zero
Onboarding a new tenantProvision infrastructure: seconds-to-minutes at best, and it can failCREATE SCHEMA + full DDL: fast but not instant, and it's a write to the catalogueINSERT one row into tenants
Per-tenant customisation (extra columns, indexes)Easy, and a trapEasy, and a bigger trapRequires a general mechanism (JSONB, Module F-13)
Cross-tenant analyticsRequires shipping data out (Modules P-22, P-23)Awkward: UNION ALL across N schemasTrivial: GROUP BY tenant_id
"Which tenant is expensive?"Read the bill (Module O-8)HardHard without per-tenant instrumentation
Compliance/data residencyStraightforward — put the silo in the regionOnly per-instanceRequires per-region deployments (Module O-9)

The honest summary of that table: silo optimises for isolation and per-tenant operations, and charges you a fleet to operate. Pool optimises for density and operational simplicity, and charges you correctness risk plus noisy neighbours. Bridge looks like a compromise on paper and behaves like the worst of both at scale.

The default for almost every new product is pool, for a reason worth stating plainly: at the start you have no idea which tenants will matter, and pool is the only model where a tenant that signs up and never returns costs you nothing. What you should build into the pool from day one is the ability to promote a tenant out of it later, which is this module's last section.


Silo's Bill Is a Migration Queue and a Cost Floor

Silo is genuinely the strongest isolation model, and the arguments for it are real: a tenant's data can be restored, exported, or deleted independently, a runaway query hurts one customer, and an enterprise buyer's security questionnaire gets easy answers. Regulated and residency-constrained deployments (Module O-9) sometimes leave you no choice.

Two costs dominate, and both scale with tenant count rather than with data volume.

The migration queue. Adding a column across 3,000 databases is 3,000 migrations. Each one can fail independently — a lock timeout on one busy tenant, a disk-full on another, a schema that drifted two years ago because someone ran a hotfix by hand. Now your deploy pipeline has partial-success semantics, which means every application version must tolerate both the old and new schema simultaneously — not just briefly during a rollout, but for as long as the slowest tenant takes. That's the expand/contract discipline from Module O-4 applied for days instead of minutes, and it needs real machinery: a migration orchestrator with per-tenant state, retries, concurrency limits so you don't saturate the shared control plane, and a dashboard showing which tenants are on which schema version. Teams underestimate this consistently, because at 20 tenants a for loop works fine and the pain arrives around the point where it's most expensive to change course.

The cost floor. A silo has a minimum price whether or not the tenant uses it: the instance, its storage baseline, its replica if you promise HA, its backups, and its share of monitoring cardinality. Ten thousand small tenants at any nonzero monthly floor is a business-model problem, not an engineering one. This is exactly why silo tends to appear as a tier — the enterprise plan — rather than as the whole architecture.

Why this matters in production: the silo model relocates your scaling problem rather than removing it. Instead of one large database with a hot partition you now have a fleet with a version-skew problem, and fleet management is the harder discipline. Module F-9's rule holds: scaling one tier relocates the bottleneck, and here the new bottleneck is your control plane.


Bridge Doesn't Break the Data Model, It Breaks the Tools

Schema-per-tenant is seductive. You get namespace isolation, pg_dump --schema=tenant_4821 for a per-tenant export, no tenant_id on every table, and one instance to operate. It works well at tens of tenants and it degrades in a way that's hard to reverse.

Catalogue pressure. Every schema multiplies the system catalogue: rows in pg_class and pg_attribute for every table, index, and column, in every schema. Forty tables per tenant across 3,000 tenants is 120,000 tables before indexes. The consequences are indirect and therefore confusing:

  • Each backend process builds and caches metadata for the relations it touches, so per-connection memory rises with how many schemas a connection has visited.
  • Autovacuum has to consider every relation, and catalogue tables themselves bloat under heavy DDL — which means onboarding load degrades planning performance for everyone.
  • Anything that scans the catalogue gets slow: \dt, information-schema queries, ORM introspection at boot, migration tools that enumerate tables before deciding what to do. Application startup can end up dominated by catalogue reads.

Backup and restore stop being routine. A single logical dump of the whole database walks every relation in every schema; the operation that took minutes at 50 tenants doesn't take proportionally longer, it takes disproportionately longer, and the restore is worse. Physical backups still work and are what you should rely on, but they restore everything or nothing — so the one advantage you adopted bridge for (per-tenant restore) is available only through the logical path that has become impractical.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.