SwadhyAI Notes

All System Design notes

System Design SaaS

Multi-Tenancy, Noisy-Neighbor Isolation & Tenant-Aware Sharding

A small startup and a Fortune 100 bank both use your B2B SaaS. On Monday the bank kicks off a million-row export. Without isolation, every other customer's dashboards stall, and your cheapest customers pay for the whale's traffic.

That is the core tension this page solves. systems share compute, storage, and ops so SaaS stays affordable; a appears when one tenant's spike consumes what others need. Isolation ranges from shared tables with a tenant_id column to dedicated clusters for enterprise whales, and the journey below is how you climb that ladder without leaking data or fairness.

Isolation journey

1

Put tenant identity on cache keys, filters, indexes, and logs so leaks become impossible.

2

Cap rate, concurrency, and database connections so one export cannot empty the pool.

3

Hash small tenants for even spread; pin whales so their load stays local.

4

Group tenants into cells that bound blast radius when shared tiers are not enough.

Try it: isolate a noisy neighbor with per-tenant caps

Raise Tenant A traffic while Tenant B keeps steady load. Without caps, B inherits queue delay from shared capacity. Enable a per-tenant limit so A gets throttled or rejected instead of starving B.

Tenant A

Demanding 200 QPS.

Tenant B (victim metric)

Steady 150 QPS · estimated p99 40 ms · fair share ~214 QPS of pool.

Pool capacity
500 QPS
Total demand
350 QPS
B latency (est.)
40 ms
Sim seconds
0

Pool 350/500 QPS admitted · B latency ~40 ms · A rejected total 0. Tenant A and B share one pool. Spike A to see noisy-neighbor latency on B.

Quota at the edge. Enforce tenant limits before work enters shared thread pools, connection pools, and databases.

Measure fairness. Track p99 by tenant during load tests; a single spike should not move unrelated tenants from green to red.

What actually happens when one tenant spikes

The Monday export story is not rare; it is the default whenever many customers share one pool. SaaS economics favor that sharing, while enterprise contracts and compliance favor isolation. A global "99.9% uptime" can hide a small tenant stuck at 90% while a whale saturates one cell, so fleet averages are the wrong lens for fairness.

Production designs therefore combine tenant-aware routing, per-tenant , and shard placement so one large customer cannot exhaust the pool. Encode everywhere: missing filters leak data across customers. and separate pools for heavy analytics keep interactive dashboards alive while exports run.

The naive claim is that a shared database, a tenant_id column, and autoscaling on average CPU are enough if application code remembers every filter. The counterexample is brutal: a support admin tool forgets the filter and returns another bank's customers; independently, a bulk export holds 80% of the connection pool and every other tenant's p99 (the slowest 1% of requests) climbs past the SLO while the cluster average still looks "fine."

How to choose an isolation model

Once you accept that shared cost and fairness pull in opposite directions, three questions decide how strong isolation must be, and the table below maps the usual answers to cost.

1. Is this a free tier or a regulated whale? Shared tables with tenant_id work for cheap tiers; a dedicated database or is what regulated whales often buy.

2. What is the blast radius you can sell? Start with at the edge; move to dedicated shards when SLAs demand it.

3. Can you restore one tenant? Per-tenant backup and restore may force stronger isolation than a shared schema can give.

ModelIsolationCost
Shared DB + tenant_idLogical onlyLowest
Schema per tenantMediumMigration heavy
Database per tenantStrongOps overhead
Cell / shard per tierConfigurableBalanced at scale

Rule of thumb

Encode tenant identity everywhere, enforce at the edge, and place large tenants where their load cannot become everyone's outage. Climb the isolation table only when product, compliance, or restore boundaries demand it.

From problem to mechanism

Here is the Monday export again, slowed down so you can see where the naive shared pool fails and what each isolation layer changes.

Problem. Many tenants share one stack. Product wants low unit cost; whales and free-tier dashboards must still meet their SLOs when one export runs hot.

Naive system. One shared database, a tenant_id column, and cluster autoscaling on average CPU. Application code is trusted to filter every query; there is no per-tenant connection budget and no placement policy for whales.

Break it. A forgotten filter returns another bank's customers. A bulk export holds 80% of the pool; other tenants' p99 burns the SLO while averages still look healthy.

Mechanism. Trusted flows through APIs, repositories, caches, and object paths. and connection budgets enforce fairness at the gateway and database proxy. places small tenants by hash and pins whales; bound blast radius. is defense in depth, with careful pool session reset so reused connections do not keep the previous tenant's context.

Correctness invariant

Derive tenant context from authenticated membership and authorization, then bind that context to every access path. A tenant identifier in a URL, request body, cache key, or object path is only a label until a trusted component checks that the caller may use it. Database row-level policies add defense in depth, but privileged roles and connection session state can bypass or confuse them if configured carelessly.

With pooled connections, prefer transaction-scoped tenant settings where supported and ensure cleanup on errors and cancellations. The next borrower must not inherit the previous tenant’s context. Exercise tenant A followed by tenant B on the same physical connection and verify access denial, including background jobs and support tooling.

Every authorization, query, cache key, queue item, metric, and audit event carries a trusted tenant identity. No tenant can consume another tenant's reserved capacity or observe its data. Tenant identity comes from authenticated context, never from an untrusted request body field alone.

Noisy-neighbor controls and placement

The mechanism names quotas and placement; these are the concrete controls that stop one tenant from starving the rest, plus how keeps load bounded.

Apply request-rate and in-flight limits per tenant before expensive work begins, then preserve the tenant identity through job queues and database access. Rate limits alone are insufficient: one permitted export can hold a connection and consume I/O for minutes. Bound export concurrency and chunk size, and give interactive requests separately protected capacity.

Weighted scheduling lets tenants consume a defined share while allowing idle capacity to be borrowed. Reclaim borrowed batch capacity by stopping new batch dispatch and letting bounded chunks finish; arbitrary cancellation can leave expensive partial work or inconsistent external effects. Measure queue age and completed work by tier and cell so a high-volume tenant cannot hide smaller tenants’ poor service.

Hashing tenant identifiers distributes the number of tenants but does not guarantee balanced load when tenant sizes vary. Track each tenant’s sustained CPU, I/O, connections, and storage footprint, then place heavy tenants according to measured demand. A tenant-to-cell directory permits deliberate moves, but needs caching and versioned ownership so a directory outage or stale route cannot create two writers. Choose rebalance thresholds with headroom for copying data and handling one-cell failure.

Worked example: cell capacity and a whale move

A cell containing its own serving and storage capacity limits the effect of a failure or overload to a subset of tenants. Shared identity, routing, and deployment control planes can still fail across all cells, so keep ordinary serving paths usable with bounded cached configuration where appropriate. Dedicated tenant databases strengthen some backup and restore boundaries, but do not automatically isolate shared compute, search, queues, or operators’ credentials.

With controls in place, here is how capacity and a whale move look in numbers. Ten thousand tenants share eight . One cell is sized for 4,000 req/s. Tenant A grows to 18% of that cell's traffic (~720 req/s) and saturates database CPU. Interactive work has a 500 ms SLO; exports may wait.

Fair capacity split

2,800 interactive + 1,200 weighted batch

Idle reservations may be borrowed, but the scheduler reclaims them when interactive queue age rises.

Hidden SLO failure

Fleet 99.9% ≠ per-tenant 99.9%

Track SLI by tier and cell. A global average can hide a small tenant burning its entire error budget.

Move protocol

copy → CDC → fence → route v42

During a move, copy the tenant’s data and stream subsequent changes while the source remains authoritative. At cutover, fence the source from further writes, establish a final replicated boundary, and publish the new routing generation. Both source and destination must enforce ownership appropriately; checking only the destination leaves stale clients able to write to the old source. Resume at the destination only after required data has caught up and validate indexes, caches, and queued jobs as part of the move.

Numbers to know

  • Cell size: 4,000 req/s; whale at ~18% (~720 req/s) can saturate database CPU alone.
  • Interactive SLO: 500 ms; split capacity 2,800 interactive versus 1,200 weighted batch.
  • Rebalance trigger: shard CPU above 70% sustained; then copy, CDC (change data capture), fence, and route.

Failure timeline, and what still breaks after the fix

Putting the pieces together, here is how a noisy-neighbor export plays out minute by minute, followed by the signals that would catch it, the ways an improved design can still fail, and the trade-offs you are buying.

Timeline: noisy neighbor export

0m

Tenant starts 500 exports

Batch workers consume database connections and I/O. Interactive dashboards for other tenants start queuing.

2m

Interactive queue age rises

Borrowed capacity has not been reclaimed quickly enough. Small-tenant p99 crosses 500 ms while fleet average still looks acceptable.

Response: preempt batch dispatch, return connections to the interactive pool, lower export chunk size.

5m

Shard remains hot

Tenant baseline now exceeds shared-cell placement policy even after .

Response: versioned shard move with CDC catch-up and source fencing.

30m

Isolation verified

Test the move from several tenant identities, including unauthorized attempts to read another tenant’s records, objects, cache entries, and search results. Run these checks while old routing caches and existing connections remain active, not only after the move completes. Also compare smaller tenants’ latency during the copy and catch-up stages; a technically correct migration can still violate the isolation promise by consuming their reserved I/O or database sessions.

Response: close only after audit, cache, search, and object-store inventories reconcile.

ASCII sketch
  whale export ──► pool exhaustion ──► small-tenant SLO burn
         │                                    │
         ▼                                    ▼
   per-tenant admission              reclaim interactive budget
         │                                    │
         └────────── still hot? ──► fence + move tenant ──► verify

What to watch

  • SLI, throttles, queue age, and cost by tier or cell, not fleet averages alone.
  • Connection, CPU, and I/O entitlement versus actual use for top tenants.
  • Routing-map version skew, move lag, and synthetic cross-tenant denial on every release.
  • Alert when one tenant takes an unsafe cell share or small-tenant SLO diverges.

Still fails when…

  • without pool reset keeps the previous tenant's session and still leaks.
  • Metrics with raw tenant_id explode cardinality; use tier or cell aggregates instead.
  • Tenant-to-cell maps without cache and versioning make the directory itself the outage.

Trade-offs

  • Shared models minimize cost; isolation minimizes blast radius and compliance risk.
  • Database-per-tenant strengthens restore boundaries but explodes connections and idle spend.
  • Hard quotas protect safety; soft quotas allow bursts, yet silent limits feel like outages.

Isolation as a product boundary

What production needs to hold

A production design needs a way to recognize trouble and a rule it preserves while recovering. A trigger is the measurement or event that starts a response. An invariant is a statement that must remain true, such as never confirming the same reservation to two customers.

Trigger metric or condition

Strengthen tenant isolation when shared-resource p99 diverges by customer, whale usage starves smaller tenants, compliance needs a narrower audit boundary, or one tenant must move without global downtime.

Correctness invariant

Every authorization, query, cache key, queue item, metric, and audit event carries a trusted tenant identity; no tenant can consume another tenant’s reserved capacity or observe its data.

Tenant identity through the shared stack

Move a whale tenant without cross-tenant impact

Ten thousand tenants share eight cells. Tenant A grows to 18% of traffic and causes one cell’s database CPU to reach 90%. Interactive work has a 500 ms SLO; exports may wait. A signed routing directory maps tenant_id to cell and map_version.

Fair capacity

cell 4,000 req/s; reserve 2,800 interactive + 1,200 weighted

Idle reservations may be borrowed, but a scheduler reclaims them when interactive queue age rises. Per-tenant burst does not imply permanent dedicated capacity.

Tenant SLO

99.9% per tenant, not only fleet aggregate

A global 99.9% can hide a small tenant at 90%. Track SLI by tier, cell, and bounded tenant exemplars without creating unbounded metric labels.

Move protocol

copy → CDC catch-up → quiesce writes → fence → route v42

The destination rejects writes with an old map_version. Read fallback is temporary; only one location owns writes after the fencing point.

Decision

Tenant A moves to a dedicated shard. Interactive and export work use separate queues and connection budgets, row-level security binds tenant_id from trusted session context, and a synthetic isolation test attempts cross-tenant reads on every release.

How the mechanism holds

Follow how each part protects the rule above. The mechanism explains what the system does; the reasoning explains why that action is sufficient under the stated assumptions. If an assumption changes, the guarantee may change too.

Tenant-key enforcement

Derive tenant identity from authenticated context, never request body. Repository APIs require tenant scope; composite primary/foreign keys include tenant_id; caches and object paths prefix it.

Why it holds: Static checks plus negative integration tests fail every missing or mismatched tenant predicate.

Row-level security

Bind tenant context on each checked-out connection and reset it before reuse. RLS is defense in depth, not a replacement for application authorization or safe background jobs.

Why it holds: A pooled connection reused across tenants cannot retain prior session state.

Tenant-aware resources

Budget rate, concurrency, DB connections, queue weight, storage, and expensive query classes by tenant/tier. Separate interactive and batch pools; cap metric cardinality with top-N views.

Why it holds: One tenant at quota cannot move another tier’s queue-age or p99 objective.

Isolation and audit boundary

Encrypt with tenant-scoped keys where required, record actor/tenant/resource in audit events, constrain support impersonation, and make backup/export/restore tenant-aware.

Why it holds: A tenant export and deletion reconciliation proves no object falls outside its inventory.

Isolation drill: a bulk export floods the cell

0m

Tenant starts 500 exports

Batch workers consume database connections and I/O.

Response: Per-tenant admission caps concurrent exports; remaining jobs queue in that tenant lane.

2m

Interactive queue age rises

Borrowed capacity has not been reclaimed quickly enough.

Response: Scheduler preempts batch dispatch, returns connections to the interactive pool, and lowers export chunk size.

5m

Shard remains hot

Tenant’s baseline now exceeds shared-cell placement policy.

Response: Initiate versioned shard move with CDC catch-up and source fencing.

30m

Isolation is verified

Synthetic tests compare small-tenant p99 and cross-tenant denial during the move.

Response: Close only after audit, cache, search, and object-store inventories reconcile.

How to choose among alternatives

Pool, silo, or bridge

Choose when: Pool small tenants, silo regulated/whale tenants, and bridge tiers through cells with explicit placement.

Caution: Database-per-tenant strengthens isolation but multiplies migrations, connections, backups, and idle spend.

Hash vs directory routing

Choose when: Hashing balances stable small tenants; a directory enables placement and moves for whales/residency.

Caution: The directory becomes a critical, cached, versioned control plane requiring safe bootstrap.

Hard vs soft quota

Choose when: Hard limits protect safety; soft limits allow controlled bursts and billing workflows.

Caution: A quota without user-visible errors, usage telemetry, and escalation becomes an outage surprise.

Running it day to day

Measurements tell you what is happening; alerts tell an operator when action is needed. A rollout plan limits how many users encounter a change before it is checked. A runbook records the steps to diagnose and recover from a known failure.

Measure

  • SLI, throttles, queue age, and cost by tier/cell/tenant sample
  • connection/CPU/I/O entitlement vs use
  • routing-map version, move lag, and cross-tenant denial tests

Alert

  • one tenant consumes unsafe cell share
  • small-tenant SLO regresses behind fleet average
  • RLS context missing or routing versions disagree

Roll out safely

  • enforce trusted tenant context in shadow logs
  • turn quotas on per tier with customer communication
  • game-day tenant move and restore before first emergency

Runbook

  • identify tenant/resource and protect interactive pools
  • throttle, isolate, or move using versioned fencing
  • reconcile data, cache, audit, billing, and customer-visible effects

Practice scenario

Design a SaaS analytics platform with 9,900 small tenants and 100 enterprise whales that need data residency and per-tenant SLOs.

Questions worth asking next

  • How does connection pooling stay fair?
  • How do you move a tenant?
  • How do you prove no cross-tenant read?

A solid answer sketch

Use regional cells, a versioned tenant directory, shared shards for small tenants, and dedicated placement for whales. Carry trusted tenant context through auth, queues, pools, RLS, cache, and audit; enforce tier quotas and use copy/catch-up/fence/cutover for moves.

Common mistakes

  • • taking tenant_id from the request payload
  • • fleet-average dashboards only
  • • RLS with pooled session state that is never reset

Last reviewed: 6 August 2026

Choosing and explaining multi-tenancy and isolation

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

Every query and cache key should carry tenant scope.

Quotas and shedding protect shared tiers from one tenant spike.

Enterprise isolation is a product tier, not an afterthought.

When it belongs

B2B SaaS with small and whale customers on same stack.

API product sells per-tenant throughput SLAs.

Compliance requires data residency per tenant.

How it fails

Missing tenant filter → cross-tenant data leak.

Shared cache key without tenant prefix.

One tenant bulk export starves interactive traffic.

Trade-offs to weigh

Shared model minimizes cost; isolation minimizes risk.

Dedicated shards for whales simplify ops but complicate empty capacity.

Per-tenant DBs simplify backup/restore; explode connection counts.

Common misunderstandings

Row-level security without application-level defense in depth.

Global counters or locks shared across tenants.

Autoscaling on cluster average while one shard melts.

Wrap-up

You have seen the failure, the mechanism, and the operational signals. Use these exercises to check the ideas stuck, then follow the references for the primary sources.

Exercises

  1. Whales and residency. Design isolation for 9,900 small tenants and 100 enterprise whales with data residency. Where do quotas live, and how do you move a whale?
  2. RLS session leak. A pooled connection runs Tenant A, then Tenant B. Explain how RLS session context can leak and how you prevent it.
  3. Hard vs soft export quotas. Choose hard vs soft export quotas for a free tier. State the customer-visible error, the billing signal, and the interactive SLO you are protecting.

Topic-specific references

Key takeaway

Multi-tenancy is a fairness and security problem. Encode tenant identity everywhere, enforce quotas at the edge, and place large tenants where their load cannot become everyone's outage.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Enforce tenant keys and row policy, isolate pools/queues/shards, set per-tenant quotas/SLOs, support moves, audits, and noisy-neighbor tests.

Explain the decision

Why is a tenant_id column not sufficient isolation?

Read the reasoning

Every query, cache key, background job, export, log, and authorization path must enforce it; row-level policy and tests reduce omission risk.

Apply it to a production scenario

One tenant consumes 70% of a shared worker queue. What changes?

Read one approach

Use tenant-aware fair queues, quotas/concurrency, reserved critical capacity, cost attribution, and an isolation/move path.

Work through the numbers

A 12,000 QPS cluster reserves 15% and splits the rest equally among 100 tenants. Base share?

Read the worked calculation

10,200 / 100 = 102 QPS each before weighted tiers and borrowing policy.

Reason through a failure

What is the tenant invariant?

Read the recovery reasoning

No request, cache entry, async task, backup, or diagnostic path can read or mutate another tenant without an explicit audited administrative capability.