All guides

LEARN \u00b7 DEBUGGING GUIDE

Tenant cache leak: how to debug cross-tenant data exposure through caching

A multi-tenant SaaS app. User in Tenant A makes a request. The response shows Tenant B's data. The cache key is `users:list` instead of `tenant:A:users:list`. Every tenant shares the same cache slot.

AdvancedDatabase/debugging

What this usually means

In a multi-tenant application, every piece of cached data must be scoped to a tenant. If the cache key does not include a tenant identifier, the first tenant that populates the cache shares its data with every other tenant. This is a data leak, not just a stale data bug. It is especially dangerous because it can go unnoticed: the data looks valid, just belonging to the wrong tenant.

( 01 )Fast diagnosis

The first ten minutes \u2014 establish facts before touching code.

  • 1Examine the cache key construction for every cache read and write. Does every key include a tenant identifier?
  • 2Log the cache key used for requests from different tenants. Do they produce different keys?
  • 3Check Redis or cache store directly. Do key names contain tenant IDs?
  • 4Trace a request from Tenant A and Tenant B for the same resource. They should hit different cache keys.
  • 5Audit all cache middleware, decorators, and helper functions. Do they have access to the tenant context?
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchCache key generation code — every function that builds a cache key
  • searchCache middleware or decorators — how they derive the cache key from the request
  • searchTenant context — is the tenant ID reliably available when cache keys are built?
  • searchRedis or cache dashboard — inspect actual keys to verify tenant prefix
  • searchMulti-tenant request routing — is the tenant resolved before or after cache operations?
  • searchAll cache invalidation code — does it also include tenant scope?
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningCache key omits the tenant ID entirely
  • warningCache key uses a tenant name instead of a tenant ID and names collide
  • warningCache key is built before tenant identification middleware runs
  • warningA shared cache instance is used without tenant namespacing
  • warningCache invalidation does not include tenant scope, so one tenant's invalidation affects all tenants
  • warningA library or framework cache layer does not know about tenancy and uses request URL alone
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildPrefix every cache key with the tenant ID: `tenant:{tenantId}:resource:{resourceId}`
  • buildBuild cache keys through a single function that enforces tenant scope
  • buildEnsure tenant resolution happens before any cache operations in the request lifecycle
  • buildAdd integration tests that verify two different tenants get different cached data
  • buildAudit all cache interactions in the codebase for tenant scope compliance
  • buildUse separate cache instances or databases per tenant if isolation is critical
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedMake the same request as Tenant A and Tenant B. The responses must differ and be correct for each.
  • verifiedCheck Redis: two different keys should exist, one per tenant.
  • verifiedClear Tenant A's cache. Tenant B's cached data must not be affected.
  • verifiedRun a test that caches Tenant A's data, makes a request as Tenant B, and verifies no cross-tenant data.
  • verifiedAudit every cache key construction site in the codebase for tenant prefix.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningAssuming a shared cache is safe without tenant namespacing
  • warningNot testing cache isolation across tenants in multi-tenant applications
  • warningUsing the request path as the sole cache key without tenant context
  • warningNot auditing cache invalidation for tenant scope
  • warningTreating a tenant cache leak as a performance bug instead of a data leak