Multi-Tenant Patterns Explained
Multi-tenancy is the practice of serving many customers, or tenants, from shared infrastructure while keeping each tenant's data isolated from every other tenant's.
Search across all documentation pages
Multi-tenancy is the practice of serving many customers, or tenants, from shared infrastructure while keeping each tenant's data isolated from every other tenant's.
Every pattern for doing this in PostgreSQL, from a tenant_id column to a fully separate database per customer, sits somewhere on one underlying spectrum between operational simplicity and isolation strength.
This page explains that spectrum directly, so the more concrete pages in this section (shared-schema queries, RLS policies, schema-per-tenant, database-per-tenant) read as points along it rather than as unrelated techniques.
The simplest pattern, shared schema with a tenant_id column, puts every tenant's rows in the same tables, distinguished only by a tenant_id value.
CREATE TABLE projects (
tenant_id uuid NOT NULL REFERENCES tenants (tenant_id),
project_id bigint GENERATED ALWAYS AS IDENTITY,
name text NOT NULL,
PRIMARY KEY (tenant_id, project_id)
);This is the cheapest pattern to operate, because one migration, one backup, and one connection pool serve every tenant at once.
Schema-per-tenant moves one level up the spectrum by giving each tenant a dedicated PostgreSQL schema with duplicated table DDL, gaining namespace isolation at the cost of running every migration once per tenant schema.
Database-per-tenant moves further still, giving each tenant a fully separate database with independent backup, restore, and upgrade cycles, which is the strongest isolation PostgreSQL offers within a single cluster.
Every step along this spectrum, from shared schema through database-per-tenant, buys stronger blast-radius containment in exchange for more infrastructure to operate and more places for a migration to fail.
Shared-schema tenancy places the entire correctness burden on every query remembering to filter by tenant_id, which is a fragile guarantee to depend on for something as serious as cross-tenant data leakage.
Row-level security (RLS) exists to make that guarantee structural instead of conventional, by attaching a policy directly to the table that PostgreSQL enforces regardless of whether the application query remembered a WHERE tenant_id = ... clause.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);RLS is a safety net layered on top of application-level filtering, not a substitute for it, because a policy still depends on the session correctly setting app.tenant_id for every connection.
The noisy neighbor problem is the shared-schema pattern's characteristic failure mode: one tenant's unusually large workload consumes disproportionate CPU, I/O, or lock contention, degrading performance for every other tenant sharing the same tables and connection pool.
Detecting it requires per-tenant observability, such as grouping pg_stat_statements or audit logs by tenant_id, since PostgreSQL itself has no native concept of a tenant to isolate resources by.
Schema-per-tenant and database-per-tenant reduce noisy-neighbor risk structurally, because each tenant's data and, in the database-per-tenant case, connection pool, are physically separated from every other tenant's.
Compliance requirements often force the isolation decision rather than leave it purely as a performance trade-off.
A regulated enterprise customer may contractually require that its data never shares physical storage or a connection pool with any other tenant, which effectively mandates database-per-tenant regardless of what shared-schema economics would otherwise suggest.
Many real SaaS products do not pick one pattern for the whole system; they run shared-schema plus RLS for most tenants and reserve schema-per-tenant or database-per-tenant for an enterprise tier willing to pay for that isolation.
That tiered approach means the connection-routing layer, not just the schema layer, has to know which pattern a given tenant uses, which adds real application complexity in exchange for serving both ends of the market from one product.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Shared schema + tenant_id | Cheapest to operate; one migration serves all tenants | Correctness depends on every query filtering correctly; noisy-neighbor risk | Early-stage SaaS, most self-serve tenant tiers |
| Shared schema + RLS | Database-enforced isolation as a safety net | Still shares physical resources; policy correctness depends on session setup | Any shared-schema product wanting defense in depth |
| Schema-per-tenant | Stronger namespace isolation; easier per-tenant data export | Migrations run once per tenant schema; harder to scale to thousands of tenants | Mid-market tenants needing more isolation without full database separation |
| Database-per-tenant | Strongest isolation; independent backup, restore, and scaling per tenant | Highest operational cost; cross-tenant reporting requires federation | Regulated or enterprise tenants with contractual isolation requirements |
Connection pooling also has to be reconsidered at each point on this spectrum, since PgBouncer pools per database, not per schema, so schema-per-tenant tenants still share a pool while database-per-tenant tenants need pool-per-database or dynamic routing.
WHERE tenant_id = ... clause in any query path is a cross-tenant data leak, which is why shared-schema tenancy is considered the weakest link on this spectrum without additional safeguards.Multi-tenant patterns run from shared schema (cheapest, weakest isolation) through schema-per-tenant to database-per-tenant (most expensive, strongest isolation).
Every concrete pattern in this section is a point on that same spectrum.
A shared schema where every tenant's rows live in the same tables, distinguished by a tenant_id column that every query must filter on.
It is the cheapest to operate but places the most weight on query correctness.
No - RLS is a database-enforced safety net that catches queries missing a tenant filter, but it still depends on the session correctly setting the tenant context.
It complements application filtering rather than replacing it.
It is when one tenant's disproportionately heavy workload degrades performance for other tenants sharing the same tables and connection pool.
It is most visible in shared-schema tenancy and shrinks as isolation strengthens.
When a bounded set of tenants needs stronger namespace isolation or easier per-tenant data export than a shared table can offer, without the full operational cost of separate databases.
It becomes harder to justify once tenant count grows into the thousands.
Most commonly when a tenant's contract or regulatory requirement mandates that its data never shares physical storage or a connection pool with any other tenant.
It is also useful when one tenant's scale genuinely needs independent backup and restore cycles.
Yes, and many real SaaS systems do - shared-schema plus RLS for most tenants, with schema-per-tenant or database-per-tenant reserved for an enterprise tier.
This requires the connection-routing layer to know which pattern each tenant uses.
PgBouncer pools by database, not by schema, so schema-per-tenant tenants still share one pool while database-per-tenant tenants typically need a pool per database or dynamic routing.
This is an operational cost that grows along with isolation strength.
Only if it is scoped to the tenant, such as UNIQUE (tenant_id, slug).
A global UNIQUE (slug) constraint incorrectly prevents two different tenants from using the same value.
Through per-tenant observability, such as grouping query activity or pg_stat_statements by tenant_id, since PostgreSQL has no native concept of a tenant to isolate resources by automatically.
This detection work is what usually triggers a move to a stronger isolation tier for that tenant.
Not necessarily for cross-tenant analytics - database-per-tenant isolates workloads well but makes any reporting that spans tenants require federation rather than a simple join.
Isolation strength and cross-tenant query convenience move in opposite directions.
Expected tenant count, compliance requirements, and whether any tenants are willing to pay for stronger isolation.
Early-stage products default to shared schema and move specific tenants up the spectrum only when a concrete requirement demands it.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17) and PgBouncer 1.x.
Reviewed by Chris St. John·Last updated Jul 15, 2026