Per-Tenant Quota Isolation

Quota isolation on a multi-tenant platform is a key-design problem before it is a rate-limiting problem. Get the key right and every tenant has an independent limit for free; get it wrong and one tenant’s growth becomes another tenant’s outage, with nothing in the configuration that looks incorrect.

Prerequisite concepts

This assumes the tenant identification and isolation-level material in multi-tenant routing strategies and the counter mechanics in rate limiting and throttling strategies.

Three levels, three different guarantees

Each component you add to the key isolates one more thing A key of route alone gives one shared bucket for the whole platform. Adding the tenant isolates tenants from each other. Adding the consumer within a tenant additionally stops one integration inside a tenant from consuming that tenant's entire allowance, which is the complaint that arrives second. key: route one bucket for everyone any tenant can starve the rest rl:orders key: route + tenant one bucket per tenant tenants isolated from each other rl:orders:acme key: route + tenant + consumer one per integration a tenant's own apps isolated too rl:orders:acme:mobile-app Each level multiplies the key count — plan the store for the third row even if you start on the second.
# Envoy 1.32+ — descriptors built from verified values, tenant first
rate_limits:
  - actions:
      - request_headers: { header_name: "x-tenant", descriptor_key: "tenant" }
      - request_headers: { header_name: "x-consumer-id", descriptor_key: "consumer" }
      - generic_key: { descriptor_value: "orders" }
# the limit service: a per-tenant-per-consumer limit under a per-tenant ceiling
domain: platform
descriptors:
  - key: tenant
    descriptors:
      - key: consumer
        rate_limit: { unit: minute, requests_per_unit: 600 }    # per integration
    rate_limit: { unit: minute, requests_per_unit: 5000 }        # tenant ceiling

Both headers must be set by the gateway from verified claims and stripped from inbound requests, or a tenant can spend another tenant’s quota by asserting their identifier — the failure described in multi-tenant routing strategies.

Shared capacity is still shared

Per-tenant limits do not add up to a platform limit Forty tenants each allowed five thousand requests a minute total two hundred thousand, against a platform that can serve sixty thousand. Isolation is preserved and capacity is not, so a global ceiling is needed underneath the per-tenant limits — and it must shed load in a way that does not fall entirely on the smallest tenants. 40 tenants × 5,000/min allowed 200,000 requests/min permitted by the sum of the quotas platform capacity 60,000 requests/min the gap is the risk, and it is normal — quotas are not reservations Add a global ceiling under the per-tenant limits so the platform sheds load before it falls over, and make the shedding proportional to consumption rather than first-come, or the smallest tenants absorb the whole reduction. Alert when the sum of allocated quotas exceeds capacity by more than your planned over-subscription ratio.

Making a 429 useful to the tenant

A rejection that does not say which limit was hit produces a support ticket. Return the headers, and include enough detail for the tenant to distinguish their own integration’s limit from their organisation’s ceiling.

Every key component must come from something the caller cannot set The tenant and consumer identifiers used in the counter key are derived from claims in a verified token and written into internal headers by the gateway. The same header names arriving from outside are stripped first, so a caller cannot spend another tenant's quota by asserting their identifier. inbound x-tenant stripped, always verified token tenant + consumer claims gateway sets the headers from the claims it verified rl:orders:acme:mobile-app a key nobody can forge Skip the strip and the whole scheme inverts: asserting a tenant header becomes a way to spend their quota.
HTTP/1.1 429 Too Many Requests
retry-after: 12
x-ratelimit-limit: 600
x-ratelimit-remaining: 0
x-ratelimit-reset: 1754236800
x-ratelimit-scope: consumer          # or: tenant

The scope header is the one that saves the conversation. Without it, a tenant whose integration is throttled while their overall usage is well under the ceiling has no way to tell whether the problem is theirs, another team’s inside their organisation, or the platform’s.

Decision matrix

Complaint Missing key component
“Another customer’s traffic throttles us” tenant
“Our batch job starves our web app” consumer within tenant
“We are throttled below our stated limit” quota shared across regions, or a hash collision
“We see 429s with plenty of quota left” a global ceiling is engaging — say so in the scope header

Gotchas and failure signals

Per-region counters multiply the effective limit by the region count. Decide whether the published quota is global or regional and say which in the documentation.

A tenant identifier with high cardinality in a metric label will overwhelm the metrics store long before the rate limiter notices. Label metrics with a tenant tier or a sampled subset, not with every identifier.

Quota changes need to take effect without a deploy, or every tier upgrade becomes a release. Read limits from a store the gateway watches.

The first tenant onboarded with no limit at all is a permanent liability. Default new tenants to the smallest tier and raise deliberately.

Bursts, and why a per-minute limit is not what tenants experience

A quota expressed per minute says nothing about what happens in the first second of that minute, and the difference is what tenants actually notice.

With a fixed window, a tenant allowed five thousand requests a minute can send all five thousand in the first second, receive them all, and then see every request rejected for the remaining fifty-nine. From the platform’s perspective the quota was honoured exactly. From the tenant’s perspective the API was available for one second in sixty, and from the upstream’s perspective it received a spike five thousand requests tall.

A sliding window smooths the accounting and a token bucket smooths the delivery, which is the one that changes what the tenant feels: a refill rate with a bounded burst allowance lets a legitimate burst through while pacing sustained traffic. Publish both numbers — the sustained rate and the burst — because a tenant who only knows the per-minute figure will design a client that sends it all at once.

Validation

  • Key includes tenant and consumer, both from verified claims
  • Inbound tenant and consumer headers stripped at the edge
  • Global ceiling exists beneath the per-tenant limits
  • 429 responses carry limit, remaining, reset and scope
  • Sum of allocated quotas monitored against real capacity
  • Quota changes applied without a gateway deploy

FAQ

What belongs in the rate limit key?

The route, the tenant, and the consumer within the tenant. Route alone gives one shared bucket for the whole platform. Adding the tenant isolates customers from each other, which handles the first complaint. Adding the consumer stops one integration inside a tenant from consuming that tenant’s whole allowance, which is the second complaint and arrives about a month later.

Do per-tenant quotas protect the platform?

No — they protect tenants from each other. Forty tenants each allowed five thousand requests a minute permit two hundred thousand against a platform that may serve sixty thousand. Quotas are not reservations, and the gap is normal over-subscription. A global ceiling underneath the per-tenant limits is what protects capacity, and it should shed proportionally to consumption rather than first-come.

What should a 429 tell the tenant?

The limit, what remains, when it resets, and crucially the scope that was hit. Without a scope header a tenant whose integration is throttled while their overall usage sits well below their ceiling cannot tell whether the cause is their own app, another team in their organisation, or a platform-wide ceiling — and that ambiguity is the entire support conversation.

Should quotas be global or per region?

Decide and then document it, because per-region counters multiply the effective limit by the number of regions and consumers will eventually notice. Regional counters are cheap and approximate; a global counter is exact and puts a cross-region hop on the request path. Most platforms use regional counters with a global backstop, and state in the documentation that the published limit is enforced per region.


Parent: Multi-Tenant Routing Strategies