Multi-Tenant SaaS Architecture Patterns
Pooled, siloed, and bridged tenancy — the isolation-versus-cost trade-off and how to pick before you have customers.

Multi-tenancy has three implementation patterns — pooled, bridged, and siloed — and the choice between them is not primarily an architecture decision. It's a pricing and sales decision with architecture consequences. Most teams start pooled, find mid-market customers demanding more isolation, add a bridged tier, then watch their first enterprise deal require a fully siloed deployment. Build the pooled model to make that migration possible.
The Three Tenancy Models
Pooled means shared everything: one database, one schema, one application deployment. Every tenant's data lives in the same tables, distinguished by a tenant_id column. This is the cheapest operational model by a significant margin. For 1,000 tenants, you're running one database cluster, one application cluster, one set of operational runbooks. The costs are data isolation risk, noisy-neighbour exposure, and a security conversation with any enterprise prospect that will be uncomfortable.
Bridged means shared compute infrastructure with per-tenant data isolation. Common implementations: schema-per-tenant in a shared database, database-per-tenant on shared compute, or separate connection pools per tenant within a shared application tier. The operational cost is higher than pooled because tenant lifecycle management becomes a provisioning workflow, but the data boundary is clearer and most mid-market customers can accept it.
Siloed means a dedicated stack per tenant: dedicated database, dedicated application instances, potentially dedicated infrastructure. This is what enterprise customers mean when they ask "is my data isolated?" They don't mean a tenant_id column. They mean a separate deployment that their security team can audit independently. Siloed is the most expensive model per tenant and the most operationally complex to manage at scale.
The Isolation vs Cost Trade-Off
The math is direct. In pooled, 1,000 tenants might cost $500/month in infrastructure — $0.50 per tenant. In siloed, each tenant gets their own stack, which might cost $50–200/month per tenant depending on the minimum viable deployment. The same 1,000 tenants in a siloed model could run $50,000–200,000/month. That's a 100-400x cost difference. At low tenant counts, siloed is manageable. At SaaS scale, it's only viable if each tenant is paying enough to justify it — which is why siloed deployments map to enterprise pricing tiers, not self-serve signups.
The blast radius argument runs in the opposite direction. In a pooled model, a single poorly-written query — a full table scan that omits the tenant_id filter, a migration that locks the shared database — degrades every tenant simultaneously. One bad actor with API access or one bad deployment can affect your entire customer base. In a siloed model, the blast radius is one tenant. A production incident that would have caused a P1 outage affecting 1,000 customers becomes a tenant-specific incident affecting one.
Most teams underestimate the pooled blast radius until they experience it. The moment you have a shared database query that causes a 10-second timeout and every tenant's dashboard shows errors simultaneously, the calculus changes.
Data Partitioning in Pooled: The Details That Break You
Row-level security implemented via application-layer tenant_id filtering is fragile. Every query must include the tenant_id predicate. Every developer must remember to add it. Every new query path is a potential data leak.
The patterns that actually hold:
Postgres row-level security (RLS) enforces the tenant filter at the database layer. Set the session-level tenant_id variable when a connection is checked out from the pool, and the database enforces it on every query automatically. A developer writing a query without a WHERE tenant_id = $1 still gets the right data, because the database adds the predicate. The risk is connection pool contamination — if a pooled connection carries the wrong tenant_id context, it exposes the wrong tenant's data. Test this aggressively.
Index your tenant_id columns, and always compound-index on (tenant_id, id) for primary key lookups and (tenant_id, created_at) for time-ordered queries. Without these, a tenant with 10 million rows in an 800 million row table forces a full scan on every query. The tenant_id filter without an index is one of the most common pooled multi-tenant performance failure modes.
The query that breaks in production: SELECT * FROM orders WHERE status = 'pending'. Missing the tenant_id filter. Returns all tenants' pending orders. Slow, incorrect, potentially a data exposure. This happens in ad hoc reporting queries, data migration scripts, and background jobs more than in application code, because those paths get less review.
The Noisy-Neighbour Problem
Rate limiting at the API layer doesn't help when the database is the bottleneck. A tenant who fires 200 concurrent requests — within their API rate limit — can exhaust your shared database connection pool. Every other tenant's requests queue behind theirs, waiting for a connection. Your API layer returns 200 OK at the rate limit boundary but the database is thrashed.
What actually helps, in order of increasing cost:
Per-tenant connection limits at the connection pooler level (PgBouncer supports this). Set a maximum number of connections any single tenant can hold simultaneously. This bounds the blast radius.
Query timeout budgets per tenant. If a tenant's query runs more than 5 seconds, kill it. Most legitimate OLTP queries don't need 5 seconds. This prevents runaway analytics queries from holding connections.
Tier migration. The tenant causing noise has outgrown the pooled model. Move them to a bridged or siloed tier. This is a revenue conversation as much as an architectural one — the tenant likely qualifies for a higher-priced plan that funds the additional infrastructure. Frame it that way.
The noisy-neighbour problem is always a leading indicator that a tenant is growing into a higher tier. Build the migration path before you need it.
How Enterprise Deals Force Your Architecture
The conversation goes like this: "Is our data isolated from other customers?" If you say "yes, by tenant_id column in a shared database," the security review will flag it and the deal will stall or die. Enterprise security teams are not wrong to flag this. Shared database multi-tenancy is a legitimate isolation model, but it requires trust in application-layer enforcement that enterprise procurement teams are not paid to extend.
The siloed tier is not primarily an architecture decision — it's a sales motion. The first time you lose an enterprise deal because you can't offer data isolation, you will build a siloed tier. Teams that build it proactively close more enterprise deals and don't build it under deadline pressure during a proof-of-concept, which is when the architecture decisions are worst.
The design implication: your pooled model should be buildable on top of a tenant provisioning system that can provision a dedicated database as easily as it creates a tenant row. If tenant creation is INSERT INTO tenants (id, name) in the pooled model, tenant creation in the siloed tier needs to provision infrastructure. Those should share the same API surface and differ only in the provisioning driver behind it.
Migration Path: Three Years, Not Three Days
The practical progression for most SaaS companies:
Year one: Launch pooled. Every tenant shares everything. Operational simplicity is the priority. Instrument tenant-level resource usage from day one — query counts, row counts, API call rates by tenant. You will need this data when you add pricing tiers.
Year two: Add a bridged tier for mid-market customers. Schema-per-tenant in a shared database cluster covers most data isolation requirements without the full cost of dedicated infrastructure. Build tenant provisioning as a code path, not a runbook.
Year three: Add a siloed tier for enterprise. This is a separate deployment pipeline, not just a database schema. Build it to run on the same application codebase with tenant-specific configuration injection. Single codebase, multiple deployment topologies.
What to preserve in the pooled design to make migration possible:
- Never let services resolve tenant configuration by querying the database directly with hardcoded connection strings. Use a tenant context object injected at request time. When a tenant moves to a siloed database, the only change is what the tenant context resolves to.
- Keep tenant provisioning as a first-class code path from day one. If the first 50 tenants were created by hand via SQL, the migration to automated provisioning will require cleaning up whatever inconsistencies accumulated.
- Log resource consumption per tenant from the start. When a tenant is causing noise in the pooled tier, you need evidence to justify the tier migration conversation. Anecdote won't close the upgrade.
For a deeper treatment of the architecture decisions behind SaaS tenancy models and their long-term implications, the multi-tenant SaaS architecture deep-dive covers the schema design and query patterns in detail.