SaaS reliability

SaaS rate limits: control the burst and the work it admits

A configured rate does not fully describe the burst a SaaS API can accept. In our 40-request model, a fixed window admits all requests within 20 milliseconds across a window boundary. A capacity-20 token bucket admits 20, while charging for assigned work admits 12. Each result depends on an explicit admission contract.

Workload
40 requests across 20 ms; 120 assigned work units
Comparison
Fixed window, three burst capacities and a work-charged bucket
Scope
Single authority and deterministic input order; no actual execution
Conceptual regulator holding a finite reserve of blue tokens and releasing them into a narrow channel beside larger amber work blocks.
Conceptual illustration of a finite burst reserve and metered admission.

Twenty per second can admit forty almost together

Suppose an API allows 20 requests in each clock-aligned second. A client sends 20 just before the next second begins and another 20 just after it begins. The limiter can accept all 40 while obeying its configured rule. The application still receives a burst that is much larger than the phrase "20 per second" might suggest.

Our deterministic admission model places the first group at 990 milliseconds and the second at 1,010 milliseconds. A fixed one-second window admits all 40 requests across that 20-millisecond span. A token bucket with a capacity of 20 and a refill rate of 20 tokens per second admits the first 20 and rejects the second group because only 0.4 token has returned.

Accepted request counts are 40 for a fixed window, 5, 10 and 20 for the three token capacities, and 12 for work-charged admission.
Figure 1. Accepted requests under five admission rules. The table also gives accepted work units, which are assigned costs rather than measured CPU time. View full-size figure.
Admission and assigned work across five model runs
Admission ruleRefill or window rateBucket capacityAccepted requestsAccepted work units
Fixed one-second window20 requests per secondNot applicable40120
Request-count token bucket20 tokens per second555
Request-count token bucket20 tokens per second101010
Request-count token bucket20 tokens per second202060
Work-charged token bucket20 work tokens per second201220

The work units are assigned inputs, not measured CPU time. Each group contains 10 cheap requests costing one work unit and then 10 expensive requests costing five. Cheap requests arrive first within a timestamp. Changing that order can change which requests are admitted, especially with work-based charging.

A token bucket has two independent controls

The bucket capacity limits the reserve available for a burst. The refill rate determines how quickly that reserve returns. With capacity B, rate r and elapsed time measured in seconds, refill is min(B, current_tokens + r * elapsed). Admit a request only when enough tokens exist for its cost, then subtract that cost.

RFC 3290's token-bucket discussion provides the rate-and-burst model in a traffic-management setting. Here we adapt the accounting units to application requests and assigned work. The example is a strict admission decision: a rejected request does not wait inside a shaping queue.

An initially full bucket is an explicit assumption. A restarted limiter that forgets its state can unintentionally grant another full reserve. A deployment that starts several independent instances can do the same at a larger scale if each instance believes it owns the entire tenant allowance.

Lowering the bucket capacity from 20 to 10 changes the observed accepted count from 20 to 10 without changing the refill rate. Capacity 5 admits five requests. These values are sensitivity examples, not recommended defaults. Choose a burst allowance from the amount of simultaneous work the downstream system can absorb and the client behavior you intend to support.

Count the resource you mean to protect

In the capacity-20 request-count run, the first group consumes all 20 tokens. Those requests represent 60 assigned work units: ten cheap operations contribute 10 and ten expensive operations contribute 50. The limiter treats both classes as equal because its unit is a request.

The work-charged run spends one token for a cheap request and five for an expensive one. It admits the first ten cheap requests and two expensive requests, consuming 20 work tokens across 12 accepted calls. That is less accepted work under this particular ordering. It does not establish that the policy is fair to expensive operations or that the assigned cost predicts actual execution.

Google SRE's discussion of overload explains why queries per second can be a poor proxy when requests have different resource requirements. Use measurements from the resource you want to protect to calibrate a cost model. A request with a tiny payload can still trigger an expensive query or a large export.

Estimated costs will sometimes be wrong. Decide what happens when actual work exceeds the estimate: enforce a runtime budget, charge the difference, restrict the operation size or require asynchronous admission. A token decision at the edge cannot cap resources it neither measures nor controls.

Keep rate, concurrency and fairness separate

A rate limiter controls admission over time. A concurrency limit controls how much admitted work remains in progress. If requests become slower, the same admitted rate can occupy more worker slots and database connections. Waiting for a token does not reserve a connection safely, and receiving a token does not prove a connection is available.

Identify the tenant, evaluate budgets, charge the rate cost atomically, reserve execution capacity and release it while recording actual work.
Figure 2. Rate budgets decide admission; execution reservations govern in-flight work. Refund and partial-debit behavior need an explicit policy. View full-size figure.
Match each resource boundary to its control
Resource or promiseControl to considerWhat it does not establish
Sustained arrival volumeTenant and endpoint rate budgetsA bound on long-running work already admitted
Short burst absorptionBucket capacityFair progress for every workload class
Occupied workers or connectionsIn-flight limit with release handlingA long-term usage entitlement
Unequal operation costCost-aware admission and runtime budgetsAccurate cost without measurement
Progress among tenantsScheduling and queue admission policySecurity isolation of tenant data

The noisy-neighbor queue experiment examines the scheduling side of this boundary. A tenant can obey a rate limit while still monopolizing a shared queue with long-running work. Queue policy and rate policy should be reviewed together, using separate metrics for accepted volume and in-flight occupancy.

The proposed request path in the diagram is:

  1. Identify the authenticated tenant and the requested work class.
  2. Evaluate the applicable tenant, endpoint and shared budgets.
  3. Charge the selected rate cost through an atomic admission decision.
  4. Acquire execution capacity or place work into a bounded queue.
  5. Release execution reservations on completion or failure and record actual usage.

If capacity cannot be acquired after tokens were charged, choose and document a refund policy. Partial refunds, no refunds and reservation-based accounting produce different client behavior. With multiple budgets, avoid consuming one allowance permanently when another rejects the same request unless that is the intended contract.

A distributed limiter needs a failure policy

The reference model has one admission authority and an ordered input list. A real fleet needs agreement about shared counters. Separate per-instance buckets can be useful as local protection, but their aggregate capacity is the sum of the independent allowances unless a coordination mechanism assigns smaller shares.

For a shared store, the refill, eligibility check and debit must behave as one decision. A client-side read followed by a later write can allow concurrent requests to spend the same balance. Define how the clock is sourced, how state expires and what happens when the store cannot be reached.

Failing open favors availability and can admit excess work. Failing closed protects the budget and can reject legitimate customers during a limiter outage. Neither choice is universally correct. Some operations can fall back to conservative local limits while expensive mutations require a stricter boundary.

Managed gateway behavior also has limits. Amazon API Gateway's REST API throttling guidance describes rate and burst settings and calls throttling best effort. Treat the documented service contract as the source for provider guarantees. The strict reference bucket in this article is not a simulation of API Gateway's implementation.

Tell clients what a rejection means

RFC 6585 defines HTTP 429 for too many requests and permits a Retry-After header. It does not prescribe how a service identifies a caller or counts requests. Your response should explain the relevant limit in a way the client can act on, without exposing another tenant's state.

For a simple token bucket, the time until a request can afford its cost is (cost - available_tokens) / refill_rate when the balance is insufficient. This is an estimate that assumes no competing request spends the replenished tokens first. If the operation costs more than the bucket's maximum capacity, waiting cannot make it eligible. The operation needs a different path or a revised contract.

A retry after a rate rejection can itself add load. Bound retries and use the provider's documented waiting guidance. For a mutation with an uncertain outcome, preserve the original operation identity as described in the API idempotency article. A fresh key is not a general way to repair a failed attempt.

Measure admitted requests, rejected requests, charged work and actual resource occupancy separately. Observe the oldest queued work and the distribution across tenants. A low rejection rate can coexist with an overloaded database if the accepted operations are expensive enough.

Replay the boundary before choosing settings

The standard-library Python model can be saved as experiment.py and run with Python 3. It writes results.json beside the script. The saved trace records each request's timestamp, assigned work, admission decision and remaining balance for every policy.

The implementation uses integer milliseconds and milli-tokens, avoiding floating-point accumulation in its admission decision. It also checks that a stream of 100 unit-cost requests spaced 50 milliseconds apart remains admitted at the refill rate, and that a request costing six tokens cannot enter a bucket with capacity five. A second complete run must match the first output.

No request actually executes. The model contains no distributed store, network delay, clock skew, concurrency reservations or queue. Use the provided cases to establish the meaning of your settings, then test the missing failure boundaries in your service. The number printed beside a rate limit becomes useful only when the burst, cost unit and coordination scope are equally explicit.

Sources

Documentation checked .

  1. RFC 3290: token buckets
  2. RFC 6585: HTTP 429
  3. AWS: API Gateway REST API throttling
  4. Google SRE: Handling Overload