Cloud Tech by Victor
System DesignIntermediate

Rate Limiting Algorithms

How the token bucket algorithm AWS API Gateway actually uses separates a steady-state rate from a burst allowance, and why a request only fails once the bucket is genuinely empty, not the instant the average rate is exceeded.

Updated 2026-07-244 min read

Overview

The token bucket is the algorithm behind most production rate limiters, including API Gateway's: tokens are added to a bucket at a configured rate (the sustained, steady-state throughput), the bucket has a maximum capacity (the burst limit), and each request consumes one token, proceeding immediately if one's available and getting rejected (typically a 429) only once the bucket is genuinely empty. This is why a short traffic spike doesn't fail instantly even though it exceeds the configured rate, unused tokens accumulated during quieter periods absorb the burst, up to the bucket's capacity, before rejection ever starts. A fixed-window counter (a simple "N requests per minute" reset on the clock) has a real, documented flaw a token bucket and sliding-window approaches both avoid: a client can send a full window's worth of requests at the very end of one window and another full window's worth at the start of the next, doubling the effective rate right at the boundary.

Quick Reference

ConceptWhat it means
RateTokens added per second - the sustained, steady-state throughput
BurstBucket capacity - the largest instantaneous spike tolerated
Token consumedOne per request; proceeds immediately if available
Bucket emptyRequest rejected (429) until tokens replenish
AlgorithmTolerates bursts?Boundary-doubling risk?
Fixed window counterNoYes - a real, documented flaw
Token bucketYes, up to burst capacityNo
Sliding windowConfigurableNo

Syntax

yaml
# API Gateway usage plan - rate is tokens/sec added, burst is bucket capacity
throttle:
  rateLimit: 100   # steady-state requests per second
  burstLimit: 200  # max concurrent burst the bucket can absorb

Examples

python
# A minimal token bucket - request proceeds if a token is
# available, rejected once the bucket is actually empty.
import time

class TokenBucket:
    def __init__(self, rate, capacity):
        self.rate, self.capacity = rate, capacity
        self.tokens, self.last = capacity, time.time()

    def allow(self):
        now = time.time()
        self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
        self.last = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

Rate and burst are independent settings, not one number

Configuring only a "requests per second" figure without a separate burst capacity either rejects legitimate short spikes too aggressively or, if burst is left too high, allows spikes far larger than intended. Set both deliberately based on real traffic patterns.

Visual Diagram

Common Mistakes

  • Using a naive fixed-window counter and not accounting for its documented boundary-doubling flaw, where a client can effectively double the intended rate right at a window edge.
  • Setting burst capacity far higher than any legitimate traffic pattern needs, defeating the purpose of having a steady-state rate limit at all.
  • Rejecting requests based on average rate alone instead of actual token availability, penalizing legitimate short bursts a properly-sized bucket would have absorbed.
  • Applying one single rate limit account-wide when different clients or endpoints have genuinely different legitimate traffic patterns, instead of layering per-client and per-method limits.

Performance

  • A token bucket's per-request check is O(1), a simple arithmetic update and comparison, negligible overhead compared to the request it's gating.
  • Sliding window implementations generally track more state (recent request timestamps or sub-window counters) than a token bucket's single counter, a real memory/complexity trade for avoiding the fixed-window boundary flaw.

Best Practices

  • Configure rate and burst as two deliberate, independent values based on real observed traffic, not a single guessed number.
  • Prefer token bucket or true sliding-window algorithms over a naive fixed-window counter, given its documented boundary-doubling flaw.
  • Layer throttling at multiple levels (account, API, per-client) when different consumers legitimately need different limits, rather than one blanket setting.
  • Return a clear, standard rejection response (429, with retry-after guidance where possible) so well-behaved clients can back off correctly.

Interview questions

In the token bucket algorithm, what do the "rate" and "burst" settings each actually control?

The rate is how many tokens get added to the bucket per second, the steady-state throughput the API is designed to sustain indefinitely. The burst is the bucket's total capacity, the maximum number of tokens that can accumulate and therefore the largest spike of concurrent requests the API will accept in a single instant before it starts rejecting anything further. A request consumes one token; as long as the bucket has a token available, the request proceeds immediately, rate and burst are two independent numbers, not one combined setting, because sustained throughput and tolerance for short spikes are genuinely different things to configure.

A client sends a sudden spike of requests that exceeds the configured rate, but the API doesn't immediately reject any of them. Why not, and when does rejection actually start?

The token bucket has accumulated capacity up to the burst limit, if the client had been under the rate limit recently, unused tokens built up in the bucket, and that reserve absorbs a short spike without any request failing, exactly the point of separating burst capacity from steady-state rate. Rejection (a 429 response) only starts once the bucket is actually empty, every accumulated token has been consumed and the spike is sustained long enough that the rate of token consumption keeps exceeding the rate of token replenishment. This is why a token bucket, unlike a hard per-second cap, tolerates brief bursts gracefully while still enforcing a real steady-state ceiling.

Why would a system use a sliding window rate limiter instead of a token bucket, and what problem does it fix?

A naive fixed window (say, "100 requests per minute, resetting on the minute") allows a client to send 100 requests in the last second of one window and another 100 in the first second of the next, 200 requests in a two-second span despite the stated 100/minute limit, an artifact of the window boundary rather than actual demand. A sliding window rate limiter instead evaluates the limit over a continuously moving time range rather than fixed, discrete buckets, which avoids that boundary-doubling effect at the cost of typically more state to track (timestamps of recent requests, not just a single counter) compared to a token bucket's simpler accumulator model.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement