Row-Level vs Schema vs Database Multi-Tenant Isolation
Schema-per-tenant breaks down past a few thousand tenants from PostgreSQL catalog bloat — row-level, schema, and database isolation compared.

Every multi-tenant SaaS product eventually asks the same question: where does one tenant's data actually stop and another's begin? The answer isn't philosophical — it's a specific database decision, made once early and expensive to reverse later. Get it wrong and you either leak data across tenants or rebuild your data layer at 10x the customer count.
There are exactly three places to draw that boundary: inside a shared table (row-level isolation), inside a shared database using separate schemas (schema-level isolation), or across entirely separate database instances (database-level isolation). Each trades isolation strength against operational cost differently, and each has a tenant count where it starts to hurt. This guide covers what each model actually looks like in PostgreSQL, where the breaking points are, and how to pick between them for embedded analytics and SaaS products generally.
Key Takeaways
- AWS's SaaS Tenant Isolation Strategies whitepaper names three architectural patterns — pool, bridge, and silo — that map directly to row-level, schema-level, and database-level isolation (AWS, 2026).
- Row-level isolation (shared tables + a tenant_id filter) has no per-tenant catalog footprint, so it scales well past the tenant counts where schema-per-tenant hits PostgreSQL catalog bloat — but the boundary depends entirely on every query applying the filter correctly.
- Schema-per-tenant is generally viable from roughly 100 to a few thousand tenants; past that, PostgreSQL's system catalogs (
pg_class,pg_attribute) grow large enough to slow metadata queries and DDL.- Database-per-tenant gives the strongest isolation and the cleanest cost attribution, but each tenant needs its own connection pool, backup schedule, and migration run — a real ceiling once you have more than a few hundred tenants.
What Are the Three Database Isolation Models for Multi-Tenant SaaS?
The three models are row-level isolation (one shared table, filtered by a tenant column), schema-level isolation (one database, one schema per tenant), and database-level isolation (one database instance per tenant). AWS's tenant isolation whitepaper calls these the pool, bridge, and silo models respectively, and frames the choice as a spectrum from maximum resource sharing to maximum isolation (AWS SaaS Tenant Isolation Strategies, 2026).
Moving from pool to silo buys you a stronger physical or logical boundary at the cost of operational overhead — more infrastructure to provision, monitor, back up, and patch per tenant. Moving from silo to pool buys you efficiency and simpler operations at the cost of relying on runtime controls, rather than a hard boundary, to keep tenants apart. Neither end of the spectrum is "more correct" — the right choice depends on tenant count, compliance requirements, and how much engineering time you can spend on data-layer operations.
Most teams treat this as a one-time architecture decision made at the start of a project. In practice, the tenant count where a given model stops working is well-documented enough that you can predict the migration before you need it — which means you can design the schema so that migration is a data movement problem, not a rewrite.
How Does Row-Level Isolation Work, and When Does It Break Down?
Row-level isolation puts every tenant's rows in the same shared tables, distinguished by a tenant_id column, and enforces the boundary with a filter applied to every query. In PostgreSQL, that filter is best enforced with Row-Level Security (RLS), a built-in feature since version 9.5 that attaches a policy predicate to a table so the database appends the tenant filter automatically, even if application code forgets to.
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON events
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Shared-schema RLS scales further than either alternative because it has no per-tenant object count at all. There's no catalog growth, no per-tenant connection pool, and no per-tenant migration loop — a single cluster keeps working as tenant count climbs in a way schema-per-tenant and database-per-tenant structurally cannot. The tradeoff is that isolation is a runtime guarantee, not a structural one: it holds as long as every connection sets app.current_tenant correctly and no role bypasses RLS. That session-context handoff is the same mechanism a secure token-based embedding layer needs to get right — the token has to carry the tenant identity all the way to the query. We cover the full policy syntax, session-variable handling, and connection-pool gotchas in a dedicated PostgreSQL RLS guide — this section is about when to reach for the pattern, not how to implement it.
Row-level isolation breaks down fastest on the "blast radius" and compliance axes AWS's whitepaper describes for the pool model: a single misconfigured index or a runaway query from one tenant runs against the same physical tables and disks as everyone else, so noisy-neighbor and outage impact are shared by default (AWS, 2026). If a prospective customer's security team requires a dedicated database as a contractual term, no amount of correct RLS policy will satisfy that requirement — it needs a different model entirely.
What Are the Tradeoffs of Schema-Level Isolation?
Schema-level isolation gives each tenant a dedicated PostgreSQL schema inside one shared database instance — the same server and storage engine, but each tenant's tables live under their own namespace (tenant_042.events rather than a shared events table with a tenant_id column). This is AWS's bridge model applied at the schema layer: tenants share the compute and the instance, but each gets a logically separate object namespace, which rules out the class of bug where a query simply forgets a WHERE clause.
CREATE SCHEMA tenant_042;
CREATE TABLE tenant_042.events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
Schema-per-tenant is generally recommended for roughly 100 to a few thousand tenants; even Citus 12 — which added dedicated schema-based sharding support to distributed PostgreSQL — still recommends the shared-table approach once you're anticipating millions of tenants (Citus Data, Citus 12: Schema-based sharding for PostgreSQL, 2023). The ceiling comes from PostgreSQL's system catalogs: every schema and table you create adds rows to pg_class and pg_attribute, and at a few thousand schemas times dozens of tables each, catalog lookups, pg_dump, and routine vacuuming all slow down as a direct consequence of that metadata volume.
There's a second, quieter cost: schema migrations. A column addition that's a single ALTER TABLE under row-level isolation becomes a loop over every tenant schema under schema-level isolation, and a failure partway through leaves some tenants on the old schema version and some on the new one until you reconcile it.
Teams that start with schema-per-tenant almost always do it for the isolation guarantee, not for performance — and the ones that later migrate off it are usually driven by DDL pain, not query slowness. Plan your migration tooling (a schema-versioning table, a runner that applies migrations schema-by-schema with retries) before you have 50 tenants, not after you have 500.
When Does Database-Level Isolation (Silo) Actually Make Sense?
Database-level isolation runs each tenant against a fully separate database instance — its own connection endpoint, its own storage, in the strictest form its own compute. This is AWS's silo model, and it's the only one of the three where a single tenant's failure, backup restore, or runaway query is structurally incapable of touching another tenant's data or performance, because there's no shared process or shared disk in the isolation boundary at all (AWS, 2026).
That guarantee is why regulated buyers ask for it by name. Enterprise contracts in healthcare, finance, and government procurement frequently specify dedicated infrastructure as a contractual requirement, not a preference — and no amount of RLS policy correctness satisfies a clause that says "dedicated database instance." Cost attribution is also simplest here: infrastructure spend maps directly to a tenant, with no need to instrument shared resources to apportion cost.
The operational cost scales linearly with tenant count in a way the other two models don't. Each tenant needs its own connection pool sized against your PostgreSQL server's max_connections ceiling — a parameter fixed at server start and, by default, around 100 connections unless explicitly raised (PostgreSQL documentation, Connections and Authentication, 2026). It also needs its own backup schedule, its own migration run, and its own monitoring surface. AWS's whitepaper lists onboarding automation and decentralized monitoring as the model's two biggest operational costs, both of which get worse, not better, as tenant count grows (AWS, 2026). Below a few hundred tenants this is manageable with a connection pooler like PgBouncer in front of each instance and infrastructure-as-code for provisioning; well beyond that, the per-tenant operational load is usually what forces a move toward a hybrid model.
Comparing the Three Isolation Models Side by Side
| Dimension | Row-level (pool) | Schema-level (bridge) | Database-level (silo) |
|---|---|---|---|
| Isolation boundary | Runtime policy on shared rows | Namespace within a shared instance | Separate instance, storage, and connections |
| Practical tenant ceiling | No catalog-driven ceiling | ~100 to a few thousand | Few hundred before ops load dominates |
| Blast radius of an outage | All tenants | All tenants on that instance | Single tenant |
| Schema migration effort | Single ALTER TABLE | Loop over every schema | Loop over every database |
| Cost attribution per tenant | Requires instrumentation | Moderate effort | Direct — infra maps to tenant |
| Satisfies "dedicated database" contracts | No | No | Yes |
Ceiling figures synthesize the tenant-count guidance from the AWS and Citus sources cited above, not a single benchmark; treat them as planning ranges, not hard limits — actual thresholds shift with table count, row width, and hardware.
How Do You Choose the Right Model for Your SaaS Product?
Most SaaS products don't stay on one model forever. They start with whichever model matches their tenant count and compliance needs on day one, then move toward AWS's bridge pattern as they grow — standard-tier tenants stay pooled for efficiency, while enterprise tenants that require or pay for dedicated infrastructure get siloed. Building the tenant boundary as a first-class concept from day one — a tenant_id on every table, even under schema- or database-level isolation, plus a tenant-resolution layer in your application — is what makes that later split possible without a full rewrite. It's the same tenant-resolution concept an MCP server needs for tenant-aware, read-only database access when an LLM is the one issuing the query instead of your application code.
A practical decision sequence:
- Do any current or near-term contracts require a dedicated database? If yes for even one tenant, you need database-level isolation for at least that tenant, likely alongside a pooled model for everyone else.
- Will you plausibly exceed a few thousand tenants? If yes, schema-level isolation is a dead end regardless of your current count — build on row-level isolation with RLS from the start.
- Is your team small and your tenant count under a few hundred? Schema-level isolation gives you the strongest structural guarantee against query bugs without RLS's reliance on session-variable discipline, and without silo's per-tenant infrastructure burden.
If you're embedding dashboards on top of whichever model you land on, the isolation layer needs to carry through to the query engine that renders the dashboard, not just the application backend. An embedding tool that connects directly to your existing PostgreSQL database — Draxlr's PostgreSQL connector is one option — can run against any of the three models, since the tenant boundary in that case is enforced by the database connection and query layer, not by the embedding tool itself.
Migrating Between Isolation Models in Practice
Migrating from schema-per-tenant to row-level isolation is the most common move, typically triggered by DDL pain or a tenant count that's approaching the low thousands. The mechanical steps: add a tenant_id column to each merged table, backfill it per source schema, create a single shared table, copy rows across with the tenant identifier attached, then drop the per-tenant schemas once RLS policies are verified against production traffic in a shadow-read period.
Migrating from row-level to database-level isolation for a single enterprise tenant is narrower in scope but higher stakes: it's usually one tenant's data being extracted into a new instance, with a cutover window where writes are paused or dual-written. Because the source of truth (the shared table with tenant_id) already isolates that tenant's rows logically, the extraction query itself is simple — the complexity is in connection routing, so your application needs to know, per request, which physical database a given tenant now lives in.
In both directions, the migrations that go smoothly share one trait: the tenant_id column (or an equivalent tenant identifier) already existed everywhere, even under a model where it wasn't strictly required for isolation. Retrofitting a tenant identifier onto tables that never had one is consistently the slowest part of any of these migrations — slower than the actual data movement.
Frequently Asked Questions
Can I mix isolation models within the same product?
Yes — this is AWS's bridge model, and it's the standard 2026 pattern for SaaS selling to both SMB and enterprise customers: most tenants share pooled, row-level-isolated infrastructure, while a subset of enterprise tenants runs in dedicated schemas or databases. The application layer needs a tenant-routing step that resolves, per request, which physical model that tenant lives under.
Does row-level security add meaningful query latency?
RLS folds into the same query plan as a manually written WHERE tenant_id = ... clause — PostgreSQL's planner treats the policy predicate the same way it treats any other filter condition, per the PostgreSQL documentation on row security policies. The real performance risk isn't the policy check itself — it's a missing index on tenant_id, which turns every filtered query into a full table scan regardless of whether the filter came from RLS or application code.
How many tenants can a single PostgreSQL schema-per-tenant setup realistically support?
Guidance from Citus Data, which built dedicated schema-based sharding into Citus 12, still recommends the shared-table (row-level) approach once you're anticipating millions of tenants, and treats schema-per-tenant as a fit up to the low thousands. The limiting factor is PostgreSQL catalog growth (pg_class, pg_attribute), not raw storage or query throughput.
Is database-per-tenant always the most secure option?
It provides the strongest structural isolation, but "most secure" depends on what you're defending against. A correctly configured RLS policy with FORCE ROW LEVEL SECURITY and a non-superuser application role closes the same cross-tenant query risk that database-per-tenant closes — the difference is that silo isolation doesn't depend on that configuration being correct, while RLS does.
Conclusion
Row-level, schema-level, and database-level isolation aren't competing "best practices" — they're three points on a spectrum AWS's tenant isolation whitepaper names pool, bridge, and silo, each correct for a different combination of tenant count, compliance requirement, and operational capacity. Row-level isolation with PostgreSQL RLS scales furthest and costs least to operate; schema-per-tenant trades that ceiling for a structural guarantee against query bugs, up to a few thousand tenants; database-per-tenant trades operational simplicity for the only boundary that satisfies a contractual "dedicated database" requirement.
The decision that matters most isn't which model you start with — it's whether you build a first-class tenant identifier into your schema from day one, so that whichever migration your growth eventually forces stays a data-movement problem instead of a rewrite.
For the full implementation details of the row-level model specifically — policy syntax, session-variable handling, and connection-pooler gotchas — see the dedicated PostgreSQL RLS guide.
About the author

Vivek is a coder and the founder of Draxlr who cares deeply about building good products. He works at the intersection of AI, SQL, dashboards, and embedded analytics, with a strong focus on making complex data workflows feel simple, useful, and fast for real teams.
If you have questions about anything in this guide, or want to compare options for your specific stack, you can email Vivek at vivek@draxlr.com, try Draxlr free, or reach out directly through the Draxlr team.

