Keeping tenants apart  / field guide
Field guide · security & identity

The leak goes around the tenant filter, not through it

Seven published cross-tenant incidents, from Steam's Christmas cache to ChatGPT's Redis client, and not one of them defeated the authorization layer. This guide reconstructs where multi-tenant isolation actually breaks, how Salesforce, Shopify, GitLab, AWS and Cloudflare place their tenants, and which tests would have caught each published leak.

26 primary sources 14 production systems 7 incidents Evidence through September 2026 Read: ~25 min
01

The territory

One system, many customers, and a promise that each customer behaves as if alone. Where that promise is made, where it is broken, and who has written down what happened.

Strip the technology names away and the problem is this: for economic reasons, many customers share one running system, and the operator promises each of them that nothing they store or do will ever reach another. The revenue side of that bargain is old and well understood; Aulbach, Grust, Jacobs, Kemper and Rittinger opened their SIGMOD 2008 paper with it, noting that "multiple tenants are often consolidated into the same database to reduce total cost of ownership" [24]. The risk side is what this guide is about, and it is best learned from the operators who have already paid it: Valve, Cloudflare, GitHub, OpenAI, Microsoft and Atlassian have each published an account of a day the promise failed.

Read those accounts together and a pattern appears that should reorganise how you spend an isolation budget. In every published incident in this corpus, the layer whose job is tenant scoping, the WHERE tenant_id = ? clause, the session check, the org-scoped query layer, worked exactly as designed. The data crossed between customers somewhere else: in an edge cache keyed only by URL, in a proxy buffer, in a reused request object, in a pooled Redis connection, in a shared build VM, in an operator's deletion script. The layers that leak are the ones that have no concept of a tenant at all. That is the surprise of this dig, and sections 2 and 4 are built around it.

34,000
Steam users who saw pages generated for someone else in about 90 minutes, Christmas 2015
1 / 3.3M
Peak rate of HTTP requests returning another customer's memory during Cloudbleed
1.2%
ChatGPT Plus subscribers whose billing details may have been visible in a nine-hour window
883
Customer sites permanently deleted by one script run against shared infrastructure

Figure 1 · The placement spectrum, and who sits where

tenant count grows,
cost per tenant falls

blast radius and noisy
neighbours force splitting

Silo: shared almost nothing

Shopify pods
full stack per shop subset

GitLab Cells
organization pinned to a cell

Split data, shared engine

Schema per tenant
catalog bloat at thousands

Database per tenant
fights the connection model

Pool: shared everything

Salesforce core
one schema, OrgID on every row

Postgres RLS pattern
shared tables, policy filter

tenant count grows,
cost per tenant falls

blast radius and noisy
neighbours force splitting

Silo: shared almost nothing

Shopify pods
full stack per shop subset

GitLab Cells
organization pinned to a cell

Split data, shared engine

Schema per tenant
catalog bloat at thousands

Database per tenant
fights the connection model

Pool: shared everything

Salesforce core
one schema, OrgID on every row

Postgres RLS pattern
shared tables, policy filter

Published systems cluster at the two ends of the spectrum; the middle models are where operational accounts report getting stuck. Sources: Salesforce, Shopify, GitLab, PlanetScale.
Diagram source

Scope. This guide covers data and performance isolation between customers of one product: where the tenant boundary lives, how identity travels with a request, and what broke in the published record. It deliberately does not cover network segmentation between corporate environments, Kubernetes cluster multi-tenancy for internal teams, or the adjacent-but-different problem of per-tenant encryption key management. Cell-based availability partitioning appears only where it doubles as a tenancy boundary; the 2026-08-29 guide in this collection covers it as a blast-radius mechanism.

02

How it is actually built

One enforcement point, six layers that can undo it. The reference shape below is reconstructed from the systems that published both their designs and their failures.

Every multi-tenant system in this corpus has the same skeleton: requests arrive carrying an identity, the identity is resolved to a tenant early, and a scoping mechanism applies that tenant to every data access. The mechanism varies, and section 3 treats the choice, but the variation matters less than something the incident record makes vivid: the request crosses far more layers than the one that enforces scoping. An edge cache, a proxy with shared buffers, an application server that reuses request objects, a connection pool whose connections outlive requests, a shared cache, a job queue whose workers inherit ambient state. Each of those layers multiplexes tenants, and most of them address their contents with keys that say nothing about tenancy.

Figure 2 · Reference request path, annotated with what each layer knows

operates on

Tenant A's request

Edge cache / CDN
tenant-blind: key is the URL
(Steam 2015)

Reverse proxy
tenant-blind: shared parse buffers
(Cloudbleed 2017)

App server
tenant-aware: context set per request
(GitHub 2021: env object reused)

Shared cache / Redis
tenant-blind unless key carries tenant
(ChatGPT 2023: pooled connection)

DB connection pool
tenant-blind: connections outlive requests

Database
tenant-aware: tenant_id column,
optionally RLS as second check

Job queue

Background workers
tenant-aware only if the job payload
carries the tenant (acts_as_tenant #126)

Control plane: provisioning,
deletion, backup, restore
(Atlassian 2022)

operates on

Tenant A's request

Edge cache / CDN
tenant-blind: key is the URL
(Steam 2015)

Reverse proxy
tenant-blind: shared parse buffers
(Cloudbleed 2017)

App server
tenant-aware: context set per request
(GitHub 2021: env object reused)

Shared cache / Redis
tenant-blind unless key carries tenant
(ChatGPT 2023: pooled connection)

DB connection pool
tenant-blind: connections outlive requests

Database
tenant-aware: tenant_id column,
optionally RLS as second check

Job queue

Background workers
tenant-aware only if the job payload
carries the tenant (acts_as_tenant #126)

Control plane: provisioning,
deletion, backup, restore
(Atlassian 2022)

The tenant filter is enforced in one place; every layer marked "tenant-blind" has produced a published cross-tenant incident. Reconstructed from the OpenAI, GitHub and Valve postmortems and the Crunchy Data RLS pattern.
Diagram source

The divergence points are at the edges of that skeleton. At one extreme, Salesforce's core platform is the maximal pool: Craig Weissman's QCon SF presentation describes one shared database and one application stack for all tenants, every physical row carrying the org identifier, with a metadata-driven runtime materialising each tenant's virtual schema from shared tables [22]. The 2008 whitepaper put the count at fifty-five thousand organisations on that shared design [27]. At the other extreme, Shopify runs pods: "an isolated instance of Shopify consisting of an individual MySQL database shard, along with other datastores like Redis and Memcached", each holding a unique subset of shops [12]. GitLab's Cells design document is the most explicit recent statement of the silo position: "Organizations serve as logical boundaries and Cells serve as physical boundaries", with cross-organization operations forced through public APIs [11].

Both ends work in production at enormous scale, which is itself informative: the spectrum position is not what decides whether you leak. What the pool end buys is unit economics and uniform operations; what the silo end buys is bounded blast radius and a story you can tell a regulated customer. What neither end buys is safety in the tenant-blind middle layers, because those layers exist in both designs. Shopify's pods still share job workers, app servers and load balancers outside the pod boundary, with the constraint that shared resources talk to one pod at a time; Salesforce's pool still terminates every tenant's traffic in shared infrastructure.

The enforcement point

Application-level scoping on every query, or a database policy. The Postgres version: enable row-level security and a policy comparing tenant_id to a per-request setting, bound with SET LOCAL inside the transaction so it cannot survive connection reuse.

Runs this way at: Crunchy Data pattern, Supabase

The tenant-blind middle

Caches, pools, proxies and reused request objects. None of them re-checks identity; each is safe only if its addressing key carries the full identity context or its state is destroyed between tenants. Section 4 shows one published failure per layer.

Failed at: Valve, Cloudflare, GitHub, OpenAI

The control plane

Provisioning, deletion, backup and restore tooling that operates across tenants by design. Its tenant-awareness must extend to recovery: a backup of intermingled tenants is not a per-tenant restore capability, as Atlassian's fourteen-day restore demonstrated.

Failed at: Atlassian; designed against at GitLab

03

The decisions that matter

Four forks, each with a published system on both sides and a condition that flips the answer.

Decision 1: Where does a tenant's data live?

Chosen
  • Shared tables with a tenant column: Salesforce for all core data, and the default Postgres guidance from PlanetScale and Crunchy Data.
  • Won on unit cost, one index serving all tenants, and uniform migrations.
Rejected
  • Schema-per-tenant: a Postgres list thread reports a migration across all tenant schemata taking almost two hours, with catalog queries repeated per tenant.
  • Database-per-tenant: PlanetScale calls it "at odds with the connection model of Postgres".
Flips when
  • A regulator or contract demands physical separation for named tenants: silo those tenants (the bridge model).
  • Tenant count stays double-digit and each tenant is large: per-database overhead stops mattering and stronger isolation is nearly free.

Decision 2: Who enforces the tenant filter?

Chosen
  • Application scoping plus database row-level security as a second, independent check (Crunchy Data and Supabase pattern).
  • Won because the two layers fail differently: a forgotten filter in code is caught by the policy.
Rejected
  • Application-only scoping: one missed query is a leak, and nothing else notices.
  • RLS-only with per-tenant database roles: collides with pooled connections and per-request identity.
Flips when
  • Policy cost dominates: Supabase's guidance shows per-row function calls and unindexed policy columns degrading queries by orders of magnitude until indexed and wrapped.
  • The store has no RLS equivalent: then invest the same effort in a query layer that refuses unscoped queries.

Decision 3: How does tenant identity travel through the process?

Chosen
  • Explicit context, scoped to the unit of work: the tenant rides the job payload, the transaction, the request struct.
  • Won because every published context leak involved ambient state outliving its request.
Rejected
  • Ambient thread-local or session state: acts_as_tenant's maintainer conceded the library "is only thread safe because it uses RequestStore to clear out the state between requests".
  • Session-level SET for RLS context: survives connection reuse under a pooler.
Flips when
  • It does not flip. Every boundary where execution outlives a request (pools, queues, worker threads, async cancellation) converts ambient context into a leak; GitHub's reused env object and Sidekiq's persisted tenant are the same bug in different runtimes.

Decision 4: How strong a wall around tenant-supplied code?

Chosen
  • AWS Lambda: hardware virtualization per tenant workload via Firecracker microVMs; the NSDI '20 paper judged containers insufficient for the combination of density and isolation.
  • Cloudflare Workers: V8 isolates in shared processes, thousands of guests per machine, side channels managed by removing timers and shared memory.
Rejected
  • AWS rejected shared-kernel container isolation for untrusted code.
  • Cloudflare rejected process-per-tenant: its docs say strict process isolation can cost roughly ten times the CPU and caps guest density.
Flips when
  • The code you run is your own: ordinary process isolation suffices and the microVM tax buys little.
  • Side-channel risk tolerance is low and density matters less: the isolate bet weakens, since Spectre-class attacks are mitigated there by policy, not hardware.

Figure 3 · Choosing a placement model

yes

no

yes, for some

no

yes

no

yes

no

Do tenants run their
own code in your system?

Give each workload a hard boundary:
microVM or isolate, plus an explicit
side-channel plan

Contractual or regulatory
demand for physical separation?

Bridge model: silo the mandated
tenants, pool the rest

Thousands of tenants,
mostly small?

Pool: shared tables, tenant column,
RLS as the second enforcement layer

Does one tenant dwarf
the others?

Partition into pods or cells;
place the largest tenants alone

Pool now, keep the tenant column
on every row so splitting stays possible

yes

no

yes, for some

no

yes

no

yes

no

Do tenants run their
own code in your system?

Give each workload a hard boundary:
microVM or isolate, plus an explicit
side-channel plan

Contractual or regulatory
demand for physical separation?

Bridge model: silo the mandated
tenants, pool the rest

Thousands of tenants,
mostly small?

Pool: shared tables, tenant column,
RLS as the second enforcement layer

Does one tenant dwarf
the others?

Partition into pods or cells;
place the largest tenants alone

Pool now, keep the tenant column
on every row so splitting stays possible

A decision path assembled from the flip conditions above; every terminal is an action. The ChaosDB entry point is deliberate: tenant-supplied code is the first question because it dominates every other consideration.
Diagram source
DecisionChosenRejectedBecauseEvidence
Data placementShared tables + tenant columnSchema- or database-per-tenant at scaleCatalog and connection overhead grow with tenant countPlanetScale, 2025; pgsql list, 2014
Filter enforcementApp scoping + RLSEither aloneIndependent failure modes; policy catches the forgotten filterCrunchy Data, 2023
Context propagationExplicit, per unit of workAmbient thread/session statePools, queues and cancellation let ambient state outlive its requestacts_as_tenant #141; GitHub, 2021
Untrusted codeMicroVM (Lambda) or isolate + mitigations (Workers)Shared-kernel containers; process-per-tenantContainers judged too weak; processes too expensive at densityFirecracker, NSDI '20; Workers security model
Shared workersShuffle-sharded assignmentDedicated workers per tenant; fully shared fleetRandom small subsets make full overlap between two tenants combinatorially rareAWS Builders' Library, 2019
The hinge across all four

Each decision is really the same question at a different layer: does tenant identity travel with the data, or is it ambient? Ambient identity is cheaper everywhere, and every published failure in section 4 is a place where ambient identity met a component that outlived the request it was ambient in.

04

What broke in production

Seven published incidents in three classes. Note what is absent: not one is a defeated authorization check.

Class 1: the tenant-blind shared layer. Four incidents, eight years apart, in four different components, with one mechanism: a layer that multiplexes tenants addressed its contents with less than the full identity context. Valve's emergency cache rule cached authenticated responses by URL. Cloudflare's parser stepped past a buffer and returned whatever neighbouring tenant's traffic sat in memory. GitHub's Unicorn server reuses a single env hash per worker, and a thread-safety bug in exception logging let one request's session reach another's response. OpenAI's redis-py client, after an asyncio cancellation, left an unread reply on a pooled connection so that the next request on that connection received the previous user's data. The component names could not be more different; the design failure is identical.

Figure 4 · The ChatGPT leak: cancellation desynchronises a pooled connection

Request B · user BRedisConnection poolRequest A · user ARequest B · user BRedisConnection poolRequest A · user Acaller cancelled beforethe reply is readborrow connection CGET history for A (on C)C returned with A's replystill bufferedborrow connection CGET history for B (on C)A's reply dequeued firstuser A's data delivered to user B
Request B · user BRedisConnection poolRequest A · user ARequest B · user BRedisConnection poolRequest A · user Acaller cancelled beforethe reply is readborrow connection CGET history for A (on C)C returned with A's replystill bufferedborrow connection CGET history for B (on C)A's reply dequeued firstuser A's data delivered to user B
Reconstructed from redis-py issue #2624 and OpenAI's postmortem. The pool hands user B a connection still carrying user A's unread reply; every later exchange on it is off by one.
Diagram source

Figure 5 · The Steam leak: an emergency cache rule with a tenant-blind key

User BStore originEdge cache ·emergency ruleUser AUser BStore originEdge cache ·emergency ruleUser Arule caches the responsekey = URL onlyGET /accountmiss, forward with A's sessionpage rendered for user AA's pageGET /accountcache hit: A's page served to B
User BStore originEdge cache ·emergency ruleUser AUser BStore originEdge cache ·emergency ruleUser Arule caches the responsekey = URL onlyGET /accountmiss, forward with A's sessionpage rendered for user AA's pageGET /accountcache hit: A's page served to B
Reconstructed from Valve's statement. Deployed under DoS pressure, the second caching configuration cached pages generated for authenticated users; the cache key did not include the user.
Diagram source

Class 2: the feature that crossed the boundary. Wiz's ChaosDB and Orca's AutoWarp are researcher discoveries rather than exploited incidents, but they are production isolation failures all the same. Cosmos DB bundled a Jupyter notebook feature, auto-enabled from 2019, whose container could reach shared infrastructure and read other customers' primary keys; Wiz reported full read, write and delete access to other tenants' databases. Azure Automation ran different tenants' jobs on the same VM, and an internal endpoint on sequential local ports handed out other tenants' managed-identity tokens. In both cases a mature, well-isolated data plane acquired a new compute feature that silently inherited the weakest isolation in the stack. The transfer is direct: every feature that executes anything on shared infrastructure re-opens the isolation question, and the review has to happen per feature, not per platform.

Class 3: the control plane that saw no boundaries. Atlassian's April 2022 outage leaked nothing, and belongs here anyway. A cleanup script offering both "mark for deletion" and "permanently delete" modes was run with the wrong mode and the wrong ID list, deleting 883 sites across 775 customers in 23 minutes. The recovery is the architectural lesson: backups existed, restore drills had passed, but restoring a subset of tenants out of shared databases meant standing up staging clusters and surgically extracting rows, in batches Gergely Orosz reported at up to about sixty tenants taking four to five days each; full recovery took two weeks. The data plane was multi-tenant; the restore path was effectively single-tenant-at-a-time.

Postmortem

Steam: cached pages for the wrong people

AssumptionCaching rules deployed under attack would only touch anonymous traffic.
What happenedA second emergency configuration "incorrectly cached web traffic for authenticated users"; users were served store and account pages generated for others.
Blast radiusAbout 34,000 users over roughly 90 minutes on 2015-12-25; billing addresses, purchase history, partial card and phone digits visible.
FixStore taken down, all caching configurations reviewed and redeployed, edge caches purged.
Design ruleA cache key is an authorization statement: anything cached from an authenticated response must carry the identity in its key, or must never be cached. Emergency mitigations need the same review as deploys.
Postmortem

Cloudbleed: the proxy returned other tenants' memory

AssumptionA parser bug in one customer's HTML rewriting feature could only affect that customer.
What happenedA buffer-end check used equality where it needed a bound; the pointer stepped past the buffer and responses included process memory holding other customers' cookies, tokens and POST bodies. Search engines cached the leaked pages.
Blast radiusLeaking from 2016-09-22 to 2017-02-18; peak window ran at about 1 in 3.3 million requests. Cleanup meant purging crawler caches across the internet.
FixFeature disabled globally within hours; the legacy parser was retired; fuzzing added around the new one.
Design ruleIn a shared proxy, memory safety is tenant isolation. One tenant's exotic feature runs in the same address space as everyone's traffic, so the blast radius of a parser bug is the whole customer base, and cached copies make the leak permanent until purged.
Postmortem

GitHub: another user's session in your browser

AssumptionThe request object handed to application code belongs exclusively to that request.
What happenedUnicorn reuses one env hash per worker, clearing it between requests; a background-thread exception logger holding a reference across that clear let one request's session cookie be attached to a different user's response.
Blast radiusFewer than 0.001% of authenticated sessions across under two weeks of cumulative exposure (2021-02-08 to 2021-03-05); GitHub invalidated all sessions to close it.
FixPatched 2021-03-05, hardened 2021-03-08, all active sessions revoked, and the object-reuse pattern audited.
Design ruleObject reuse for throughput turns memory lifetime into a security boundary. Anything that escapes the request scope (a logger, a metrics buffer, a closure) must copy, not reference, request state.
Postmortem

ChatGPT: a cancelled request poisons the pool

AssumptionA pooled connection returned by a cancelled request is as clean as any other.
What happenedAn asyncio cancellation between send and receive left a reply buffered on the connection; subsequent requests on it read the previous request's data, one reply behind, until disconnect. A morning deploy spiked cancellations and surfaced years-latent behaviour.
Blast radiusChat history titles visible across users; billing details of 1.2% of active ChatGPT Plus subscribers potentially visible in a nine-hour window on 2023-03-20.
Fixredis-py patched to disconnect on cancellation; the first fix was incomplete and CVE-2023-28859 covered the remainder. OpenAI added reconciliation checks that returned data belongs to the requesting user.
Design ruleCancellation is a first-class path through every connection-oriented client. If a protocol pairs requests to responses by ordering, an interrupted exchange must destroy the connection, and the application should verify that data coming back belongs to the identity that asked.
Case study

ChaosDB: the notebook next to the database

AssumptionA convenience feature bundled with a hardened multi-tenant database inherits the database's isolation.
What happenedWiz researchers escaped the auto-enabled Jupyter notebook container, reached shared cluster infrastructure, and read other customers' Cosmos DB primary keys, granting full read, write and delete on their data.
Blast radiusThousands of accounts including Fortune 500 companies; Microsoft advised key rotation because exposure duration could not be bounded. Disclosed 2021-08.
FixFeature disabled within 48 hours of the report; notebooks no longer auto-enabled; affected customers told to rotate keys.
Design ruleIsolation reviews attach to features, not platforms. Any feature that executes customer code, however incidental, resets the platform's effective isolation to that feature's isolation.
Case study

AutoWarp: tokens on the port next door

AssumptionSandboxes of different tenants on one VM cannot reach each other's credentials.
What happenedAzure Automation ran tenants' jobs on shared VMs with an internal HTTP endpoint per job on sequential ports; requesting neighbouring ports returned other tenants' managed-identity tokens.
Blast radiusOrca observed tokens for major enterprises across a scan window; Microsoft found no evidence of misuse. Reported 2021-12-06, fixed 2021-12-10, disclosed 2022-03-07.
FixAccess to the token endpoint restricted to the owning job's context.
Design ruleLocalhost is not a trust boundary on shared compute. Credentials must be bound to the identity of the requester, not to the network position of the request.
Postmortem

Atlassian: one script, 883 tenants, fourteen days

AssumptionTested backups meant tenant-level recovery was a solved problem.
What happenedA script with both "mark for deletion" and "permanently delete" modes ran with the wrong mode and the wrong IDs, deleting 883 sites (775 customers) in 23 minutes on 2022-04-05.
Blast radiusUp to 14 days of downtime for affected customers; restoring a tenant subset from shared-database backups required staging clusters and row-level extraction, in reported batches of up to about 60 tenants taking 4 to 5 days each.
FixDeletion capability withdrawn from most services; soft-delete with a 14-day suspension ahead of any permanent deletion; multi-site restore automation.
Design ruleThe restore unit must equal the blast-radius unit you promise customers. If the tenant is the unit of harm, per-tenant point-in-time restore is a product requirement, and "permanently delete" deserves the same friction as key destruction.
Source

acts_as_tenant: the tenant that rode along to the next job

AssumptionThread-local tenant context set for one unit of work is gone by the next.
What happenedSidekiq worker threads persisted the previous job's tenant; later jobs silently ran scoped to the wrong tenant. The fix PR sat from 2016 and closed unmerged in 2019, leaving mitigation to user code and a third-party gem.
Blast radiusNo published production incident; the failure mode is silent misdirection of reads and writes, which is exactly why none is published.
FixClear request-store state around every job, or carry the tenant explicitly in the job payload.
Design ruleA rejected fix is still a finding: if a library's isolation depends on state being cleared between units of work, that clearing is your responsibility at every executor you add.

Figure 6 · Eight years of cross-tenant incidents, each in a different layer

2015Steam · edge cachekey2016-17Cloudflare · proxyparse buffer2021GitHub · reusedrequest envAzure Cosmos DB ·bundled notebooks2022Azure Automation ·shared job VMAtlassian ·control-plane delete2023ChatGPT · pooledRedis connectionPublished cross-tenant failures by layer
2015Steam · edge cachekey2016-17Cloudflare · proxyparse buffer2021GitHub · reusedrequest envAzure Cosmos DB ·bundled notebooks2022Azure Automation ·shared job VMAtlassian ·control-plane delete2023ChatGPT · pooledRedis connectionPublished cross-tenant failures by layer
The same failure shape recurs while the leaking component moves through the stack; no incident in this corpus involved a defeated tenant filter. Sources are the postmortems and disclosures cited in the cards above.
Diagram source
The class with no postmortem

The plain missing-filter bug, an endpoint that forgets the tenant clause, is almost absent from this record: it lives in bug-bounty writeups and disclosure notes rather than operator postmortems. Read that absence carefully. It does not mean the class is rare; it means it is usually caught quietly, reported by a researcher, or never detected, because a correct-looking response to the wrong tenant fires no alert. Detection for this class has to be built (rung 1 of section 7), not waited for.

05

Numbers you can plan against

What the published record supports. Every figure carries its context and date; leak rates in particular are properties of specific bugs, not of architectures.

MetricValueAtContextAs ofSource
Users served another user's page~34,000Valve / SteamEmergency cache rule live about 90 minutes2015-12Valve statement
Requests leaking memory, peak window1 in 3.3MCloudflare2017-02-13 to 2017-02-18; leak had run since 2016-09-222017-02postmortem
Authenticated sessions misrouted<0.001%GitHubUnder two weeks of cumulative exposure; all sessions revoked2021-03postmortem
Plus subscribers with billing details exposed1.2%OpenAIActive users in a nine-hour window, 2023-03-202023-03postmortem
Sites deleted by one script run883Atlassian775 customers, 23 minutes; restores ran 2022-04-08 to 2022-04-182022-04PIR
Per-tenant restore throughput~60 / 4-5 daysAtlassianReported batch size and elapsed time per batch during recovery2022-04Pragmatic Engineer
Migration across all tenant schemas~2 hourspgsql list reportSchema-per-tenant Postgres, thousands of schemata, catalog queries repeated per tenant2014-11thread
Organisations on one shared pool design55,000+SalesforceVendor figure for the whole platform in the 2008 whitepaper; per-instance counts not published2008whitepaper
Guest memory ceiling per isolatelow MBCloudflare Workers"Each guest cannot take more than a couple megabytes of memory"; thousands of guests per machine2025security model
CPU cost of process-per-tenant~10×Cloudflare WorkersVendor comparison of strict process isolation against shared-process isolates2025security model
Events served by one multi-tenant query engine~100TDatadogHusky event store; reader isolation against noisy neighbours is a stated design goal2025-10Husky posts
Distinct 2-worker shuffle shards from 8 workers28AWSThe combinatorial basis for noisy-neighbour containment without dedicated hardware2019Builders' Library
Read these carefully

The incident rates are measured by the operators involved; the Salesforce tenant count, the Workers overhead figures and the shuffle-shard arithmetic are vendor statements serving an argument. Two quantities nobody publishes: the cost delta of silo versus pool at a stated tenant count, and the base rate of missing-filter bugs. Estimates of either in vendor decks are unsourced; treat them as sales material.

06

The evidence wall

Every source behind this page, graded. Filter by kind. The ledger with per-claim quotes ships alongside as sources.md.

Postmortem OpenAI2023-03

March 20 ChatGPT outage: here's what happened

The redis-py cancellation bug as experienced by its most famous victim: chat titles across users, and billing details of 1.2% of active Plus subscribers in a nine-hour window.

Carry forwardVerify at the application layer that returned data belongs to the requesting identity; the client library is not the last line.
openai.com/index/march-20-chatgpt-outage
Source redis-py2023-03

Issue #2624: cancelled command leaves connection in unsafe state

The mechanism in reproducible form, three days before OpenAI's postmortem: cancel between send and receive and every later response on the connection is off by one.

Carry forwardOn cancellation, disconnect. A connection whose request/response pairing is positional cannot be safely returned to a pool mid-exchange.
github.com/redis/redis-py/issues/2624
Source GitHub Advisory DB2023-03

GHSA-24wv-mv5m-xv4h (CVE-2023-28858)

The advisory records that the first fixed versions were "believed to be incomplete", with CVE-2023-28859 assigned to the remainder: even the patch for a cross-tenant bug needed a second pass.

Carry forwardTreat the first fix of a connection-state bug as a hypothesis; keep the reconciliation check that detects wrong-identity data after the patch lands.
github.com/advisories/GHSA-24wv-mv5m-xv4h
Postmortem Cloudflare2017-02

Incident report on memory leak caused by parser bug

The fullest public account of a shared-proxy memory leak: rates, window, the equality-check root cause, and the cleanup problem of leaked data cached by crawlers.

Carry forwardIn shared data planes, memory safety is a tenancy control; fuzz the parsers and count crawler caches as part of your blast radius.
blog.cloudflare.com/incident-report-on-memory-leak…
Postmortem GitHub2021-03

How we found and fixed a rare race condition in our session handling

An unusually candid walk from symptom to root cause: Unicorn's single reused env hash, a thread-unsafe exception logger, and sessions misrouted at under 0.001%.

Carry forwardAudit every place request state escapes request scope; performance-motivated object reuse is a tenancy boundary in disguise.
github.blog/2021-03-18-how-we-found-and-fixed…
Postmortem Atlassian2022-04

Post-Incident Review on the April 2022 outage

The definitive account of tenant blast radius in the control plane: wrong mode, wrong IDs, 883 sites gone, and a restore path that had never been exercised for a subset of tenants at once.

Carry forwardDrill restoring N arbitrary tenants to a point in time, where N matches a realistic incident, before you need to.
atlassian.com/engineering/post-incident-review-april-2022-outage
Eng blog Pragmatic Engineer2022-04

The Scoop: inside the longest Atlassian outage

The independent account, sourced from affected customers and insiders while the incident ran; supplies the restore batch sizes and elapsed times the PIR leaves vague.

Carry forwardRestore throughput, not backup existence, is the number that determines outage length; ask for it in vendor reviews.
newsletter.pragmaticengineer.com/p/scoop-atlassian
Postmortem Valve (via The Register)2015-12

Steam Christmas caching incident, Valve's statement

The operator statement, reproduced in full by the press: a DoS mitigation deployed a caching configuration that "incorrectly cached web traffic for authenticated users" for about 34,000 people.

Carry forwardPre-approve the cache rules you will deploy under attack; a mitigation written during the incident skips the review that would catch a tenant-blind key.
theregister.com/2015/12/30/steam_security_blip_explained
Case study Wiz Research2021-08

ChaosDB: how we hacked thousands of Azure customers' databases

Escape from an auto-enabled notebook container to shared Cosmos DB infrastructure and other customers' primary keys; the canonical feature-crosses-the-boundary case.

Carry forwardInventory every feature that executes anything on shared infrastructure; each one carries the platform's whole isolation promise.
wiz.io/blog/chaosdb-how-we-hacked-thousands-of-azure-customers-databases
Case study Orca Security2022-03

AutoWarp: cross-account vulnerability in Azure Automation

Different tenants' automation jobs on one VM, and managed-identity tokens served by port number rather than by requester identity. Reported, fixed in four days, disclosed three months later.

Carry forwardBind credential issuance to the requesting workload's identity; network position on shared compute proves nothing.
orca.security/resources/blog/autowarp…
Design doc GitLab2022-, ongoing

Cells architecture design document

A rare public record of an operator re-partitioning a monolithic SaaS around a tenancy unit: Organizations as logical boundary, Cells as physical, cross-cell access only via public APIs.

Carry forwardChoose the tenancy unit explicitly and make every feature declare its scope against it; retrofitting the unit later is a multi-year programme.
gitlab.com/…/design-documents/cells/_index.md
Eng blog Shopify2018

A pods architecture to allow Shopify to scale

The silo position argued from operations: full isolated instances per shop subset, chosen to bound database incidents and noisy neighbours to a fraction of merchants.

Carry forwardThe pod boundary is only as good as the rule that shared resources talk to one pod at a time; write that rule down and enforce it in code.
shopify.engineering/a-pods-architecture-to-allow-shopify-to-scale
Eng blog Shopify2021-09

Shard balancing: moving shops with zero downtime at terabyte scale

Tenant placement as a continuous process: binlog-streamed shop moves between shards, because a static assignment of tenants to silos decays into hot spots.

Carry forwardIf you silo, budget for a placement service and online tenant moves from day one; rebalancing is the steady state, not an exception.
shopify.engineering/mysql-database-shard-balancing-terabyte-scale
Eng blog Crunchy Data2023

Row level security for tenants in Postgres

The reference RLS pattern: policy on tenant_id against a per-request setting, with the transaction-scoped binding that survives connection pooling.

Carry forwardBind tenant context with SET LOCAL inside the transaction; a session-level setting is ambient state waiting for a pooler.
crunchydata.com/blog/row-level-security-for-tenants-in-postgres
Vendor Supabase2024

RLS performance and best practices

What the policy costs and how to pay less: index the policy columns, wrap volatile calls in scalar subqueries so they evaluate once, and repeat the filter in the query for the planner.

Carry forwardAn RLS policy is a predicate on every query; treat the policy column like any hot index column and benchmark before enabling on large tables.
supabase.com/docs/guides/troubleshooting/rls-performance…
Source PostgreSQL hackers list2014-11

Performance of information_schema with many schemata and tables

A practitioner running schema-per-tenant reports catalog operations collapsing at thousands of schemas, with one all-tenant migration taking close to two hours.

Carry forwardSchema-per-tenant multiplies catalog rows by tenant count; test migrations and pg_dump at ten times your projected tenant count before committing.
postgresql.org/message-id/CABZYQRKnp…
Eng blog PlanetScale2025

Approaches to tenancy in Postgres

A current, vendor-adjacent but concrete comparison of shared-schema, schema-per-tenant and database-per-tenant, honest about the connection-model problem of the last.

Carry forwardDatabase-per-tenant trades a filter you must remember for a connection string you must route; the leak moves to the router.
planetscale.com/blog/approaches-to-tenancy-in-postgres
Source acts_as_tenant2016-2019

PR #141: explicitly use nil tenant in Sidekiq server (closed unmerged)

The rejected PR this guide was required to find, and it earns its place: worker threads persisting the previous job's tenant, a maintainer acknowledging the fragility, and no merged fix.

Carry forwardWhen a multi-tenancy library's issue tracker shows a context-leak thread, assume the leak is yours to close at every executor: jobs, schedulers, listeners.
github.com/ErwinM/acts_as_tenant/pull/141
Eng blog Datadog2023-2025

Husky: multi-tenancy at scale, and inside the query engine

Performance isolation as an architecture driver: compute separated from storage partly so the query path can be isolated per workload, against poison-pill queries and noisy neighbours, at around 100 trillion events.

Carry forwardData isolation and performance isolation are separate budgets; a tenant who can slow every neighbour is a tenancy failure with no data leak.
datadoghq.com/blog/engineering/husky-deep-dive
Paper AWS / NSDI '202020-02

Firecracker: lightweight virtualization for serverless applications

The written-down reasoning for moving Lambda's tenant boundary to hardware virtualization: containers judged insufficient for untrusted code at multi-tenant density, so the VMM was shrunk until VMs were cheap.

Carry forwardWhen the workload is untrusted, pick the isolation boundary first and engineer its cost down; do not pick the cheap boundary and try to harden it.
usenix.org/system/files/nsdi20-paper-agache.pdf
Vendor Cloudflare2025 snapshot

Workers security model (docs)

The opposite bet, documented: isolates confining thousands of guests per machine, timers frozen and shared memory removed against Spectre, and suspicious workloads dynamically evicted into their own process.

Carry forwardSide channels are managed, not eliminated, in shared-process isolation; ask any isolate-based platform what its equivalent of dynamic process isolation is.
github.com/cloudflare/cloudflare-docs/…/security-model.mdx
Talk Salesforce / QCon SF2008

Craig Weissman: the internal design of Force.com's multi-tenant architecture

The chief architect explaining the maximal pool from the inside: one schema for all tenants, org identifier on every row, and a metadata runtime in place of per-tenant DDL.

Carry forwardFull pooling at this scale works only because tenants never touch physical schema; if tenants can issue DDL, the pool model is already broken.
infoq.com/presentations/SalesForce-Multi-Tenant-Architecture…
Talk Cloudflare / QCon SF2019

Kenton Varda: fine-grained sandboxing with V8 isolates

The density argument for isolates over containers and VMs, from the platform's architect, with the security trade-offs stated rather than waved away.

Carry forwardIsolation strength and tenant density trade against each other through memory overhead; know which side of that trade your unit economics require.
infoq.com/presentations/cloudflare-v8
Paper TU München et al. / SIGMOD2008-06

Multi-tenant databases for software as a service: schema-mapping techniques

The paper that framed pooling as a cost decision and catalogued the schema-mapping price of it; still the clearest statement of why the middle of the spectrum is hard.

Carry forwardConsolidation is a total-cost-of-ownership play; the isolation work it creates is the interest on that saving, paid forever.
dl.acm.org/doi/10.1145/1376616.1376736
Vendor AWS (Tod Golding)2020-08

SaaS tenant isolation strategies whitepaper

The silo / pool / bridge vocabulary the industry now speaks, with the honest framing that pooling "adds a level of complexity to the isolation story".

Carry forwardUse the bridge model deliberately: silo the tenants whose contracts demand it, pool the rest, and keep one codebase across both.
docs.aws.amazon.com/whitepapers/latest/saas-tenant-isolation-strategies
Vendor AWS (Colm MacCárthaigh)2019

Workload isolation using shuffle-sharding

The combinatorial trick for noisy-neighbour containment on shared fleets: give each tenant a random small worker subset, and two tenants sharing all workers becomes vanishingly unlikely.

Carry forwardShuffle sharding buys single-tenant-like blast radius at pool prices, but only for failures that follow the sharded resource; it does nothing for a shared cache key.
aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding
Vendor Salesforce2008-10

The Force.com multitenant architecture (whitepaper)

The written companion to Weissman's talk: metadata-driven virtual schemas over shared physical tables, and the platform-wide tenant count of the era.

Carry forwardThe pool model's real dependency is a runtime that interposes on every access; the org filter is compiled into the query path, not left to application discipline.
developerforce.com/media/…/Force.com_Multitenancy_WP_101508.pdf
07

Build a miniature, then productionise it

Seven rungs. The first three take an evening each; the line from toy to production-shaped is crossed at rung four, where the bugs from section 4 become reproducible.

Two tenants, one schema, and a hostile test suite

Build the smallest CRUD app with shared tables and a tenant_id column. Then write the test that matters: for every endpoint, authenticate as tenant B and request tenant A's object ids, expecting 404s. Delete one filter to prove the suite fails the build.

Done when: a seeded missing-filter bug cannot pass CI.  Teaches: cross-tenant denial is a tested property, not a code-review habit; this is the detection the "no postmortem" class never gets.

Row-level security as the second opinion

Enable RLS with a policy on current_setting('app.tenant_id'), bound via SET LOCAL in a transaction per request, following the Crunchy Data pattern. Re-seed the rung-1 bug and watch the policy catch what the application missed.

Done when: the seeded bug returns zero rows instead of another tenant's rows.  Teaches: two enforcement layers with independent failure modes, and the fail-closed default.

Put a pooler in the middle and hunt the ambient context

Add PgBouncer in transaction mode. Change the rung-2 binding from SET LOCAL to session-level SET and run concurrent requests for both tenants until one reads the other's rows; then change it back.

Done when: you can produce the leak on demand with SET and cannot with SET LOCAL.  Teaches: the acts_as_tenant and GitHub failure shape: ambient identity dies at any boundary where execution outlives the request.

Add a cache and fuzz its keys

Cache rendered responses in Redis. Write a fuzzer that replays each tenant's authenticated URLs as the other tenant and diffs the bodies. Then reproduce Steam's incident deliberately: cache one authenticated route keyed by URL alone.

Done when: the fuzzer catches the deliberately broken key and passes on (tenant, URL) keys.  Teaches: the cache key is part of the authorization model, and the fuzzer is cheap enough to run in CI forever.

Cancel requests mid-flight

Drive the app with a load generator that cancels a percentage of in-flight requests. Use an async Redis client and watch for cross-request responses (redis-py <4.5.3 reproduces the historical bug faithfully; current clients should survive). Add an application-level check that cached values embed the tenant id they belong to.

Done when: one hour at 5% cancellation yields zero wrong-tenant reads, verified by the embedded ids.  Teaches: cancellation as an adversary, and reconciliation as the safety net the client library cannot provide.

Restore one tenant without touching the others

Take scheduled backups of the shared database, then run Atlassian's drill in miniature: delete tenant A at a known time and restore A alone, to that point, while tenant B keeps writing with no downtime.

Done when: tenant A is back within an hour, tenant B's writes are untouched, and the runbook is written down.  Teaches: the gap between having backups and having per-tenant restore, which is measured in days at production scale.

Add a greedy tenant and contain it

Give tenant B a pathological workload (unindexed scans, hot loops). Measure tenant A's p99 before and after per-tenant connection quotas, statement timeouts, and a shuffle-sharded worker assignment across four app processes.

Done when: B saturates its shard and A's p99 stays within 20% of baseline.  Teaches: performance isolation as its own discipline, separate from data isolation, per Datadog's Husky design and the shuffle-sharding argument.

08

Keep hunting

The queries that found this material, grouped by what they surface. The vocabulary is the value: "cross-tenant", "noisy neighbour", "silo pool bridge" and "shuffle sharding" each unlock a literature that "multi-tenancy best practices" never reaches.

Incidents and disclosures

  • "cross-tenant" vulnerability postmortem OR "incident report"
  • "saw another user's" OR "generated for other users" incident
  • "incorrectly cached web traffic for authenticated users"
  • site:wiz.io OR site:orca.security cross-tenant isolation research

The mechanism in the code

  • repo:redis/redis-py cancel connection unsafe state
  • "wrong tenant" sidekiq OR celery OR worker site:github.com
  • is:pr is:closed is:unmerged tenant leak thread
  • "race condition" session "returned to the pool"

Designs and decision records

  • gitlab cells design document organization isolation
  • "tenant isolation" silo pool bridge whitepaper
  • "pods architecture" OR "cell-based" shard blast radius engineering
  • "shuffle sharding" noisy neighbor workload isolation

The database layer's fine print

  • "row level security" multi-tenant performance current_setting index
  • "schema per tenant" migrations slow information_schema thousands
  • "database per tenant" connection model postgres trade-offs
  • pgbouncer "SET LOCAL" tenant rls transaction pooling
09

References

All links were live on 2026-09-18. This session's research environment could fetch github.com and gitlab.com directly; other pages were read through a search layer, and the evidence ledger (sources.md) marks which quotes are near-verbatim on that account.

  1. OpenAI, "March 20 ChatGPT outage: here's what happened" openai.com, 2023-03-24. Checked 2026-09-18.
  2. redis-py issue #2624, "Off by 1 - Canceling async Redis command leaves connection open, in unsafe state for future commands" github.com, 2023-03-17. Checked 2026-09-18.
  3. GitHub Advisory Database, GHSA-24wv-mv5m-xv4h (CVE-2023-28858) github.com, 2023-03-26. Checked 2026-09-18.
  4. Cloudflare, "Incident report on memory leak caused by Cloudflare parser bug" blog.cloudflare.com, 2017-02-23. Checked 2026-09-18.
  5. GitHub, "How we found and fixed a rare race condition in our session handling" github.blog, 2021-03-18. Checked 2026-09-18.
  6. Atlassian, "Post-Incident Review on the Atlassian April 2022 outage" atlassian.com, 2022-04-29. Checked 2026-09-18.
  7. Gergely Orosz, "The Scoop: inside the longest Atlassian outage of all time" newsletter.pragmaticengineer.com, 2022-04. Checked 2026-09-18.
  8. The Register, "Cache-astrophic" (carrying Valve's full statement) theregister.com, 2015-12-30. Checked 2026-09-18.
  9. Wiz Research, "ChaosDB: how we hacked thousands of Azure customers' databases" wiz.io, 2021-08-26. Checked 2026-09-18.
  10. Orca Security, "AutoWarp: Azure Automation vulnerability" orca.security, 2022-03-07. Checked 2026-09-18.
  11. GitLab, Cells architecture design document gitlab.com, created 2022-09-07, ongoing. Checked 2026-09-18.
  12. Shopify, "A pods architecture to allow Shopify to scale" shopify.engineering, 2018. Checked 2026-09-18.
  13. Shopify, "Shard balancing: moving shops confidently with zero-downtime at terabyte-scale" shopify.engineering, 2021-09. Checked 2026-09-18.
  14. Crunchy Data, "Row level security for tenants in Postgres" crunchydata.com, 2023. Checked 2026-09-18.
  15. Supabase, "RLS performance and best practices" supabase.com, maintained docs. Checked 2026-09-18.
  16. PostgreSQL lists, "Performance of information_schema with many schemata and tables" postgresql.org, 2014-11. Checked 2026-09-18.
  17. PlanetScale, "Approaches to tenancy in Postgres" planetscale.com, 2025. Checked 2026-09-18.
  18. acts_as_tenant PR #141, "Explicitly use nil tenant in sidekiq server" github.com, opened 2016-06-16, closed unmerged 2019-12-23. Checked 2026-09-18.
  19. Datadog, "Husky: exactly-once ingestion and multi-tenancy at scale" datadoghq.com, 2023-02; query-engine follow-up 2025-10. Checked 2026-09-18.
  20. Agache et al., "Firecracker: lightweight virtualization for serverless applications", NSDI '20 usenix.org, 2020-02. Checked 2026-09-18. Scale phrasing corroborated via Adrian Colyer's review.
  21. Cloudflare, Workers security model (docs source) github.com/cloudflare/cloudflare-docs, 2025 snapshot. Checked 2026-09-18.
  22. Craig Weissman, "The internal design of Force.com's multi-tenant architecture", QCon SF infoq.com, 2008 talk, published 2009. Checked 2026-09-18. Video host unreachable from this environment; cited to the published abstract and companion whitepaper.
  23. Kenton Varda, "Fine-grained sandboxing with V8 isolates", QCon SF infoq.com, 2019. Checked 2026-09-18. Video host unreachable from this environment; overhead claims corroborated by [21].
  24. Aulbach, Grust, Jacobs, Kemper, Rittinger, "Multi-tenant databases for software as a service: schema-mapping techniques", SIGMOD 2008 dl.acm.org, 2008-06. Checked 2026-09-18.
  25. Tod Golding / AWS, "SaaS tenant isolation strategies" whitepaper docs.aws.amazon.com, 2020-08, marked historical by AWS. Checked 2026-09-18.
  26. Colm MacCárthaigh / AWS Builders' Library, "Workload isolation using shuffle-sharding" aws.amazon.com, 2019. Checked 2026-09-18.
  27. Salesforce, "The Force.com multitenant architecture" whitepaper developerforce.com, 2008-10. Checked 2026-09-18.