API Rate Limiting Strategies Compared -- Token Bucket vs Fixed Window vs Sliding Log

Every public API needs rate limiting. Without it, a single misbehaving client can saturate your backend, degrade service for everyone else, and run up your infrastructure costs. But choosing the right rate limiting algorithm is not trivial. The three most widely adopted strategies -- Token Bucket, Fixed Window, and Sliding Log -- each make different trade-offs between accuracy, memory usage, and burst behavior. This article breaks down how each algorithm works, walks through code examples, and helps you decide which one fits your API.

Why Rate Limiting Matters

Rate limiting controls how many requests a client can make in a given time period. It is the first line of defense against abuse and accidental overload. Beyond security, rate limiting also enables tiered pricing models. Many API providers, including ProfitEngine API on RapidAPI, offer a free tier with a defined request quota -- for instance, 60 requests per minute -- and higher limits on paid plans. That kind of tiered access is built on top of a rate limiter. A well-chosen algorithm keeps the system fair, predictable, and performant for every user.

Token Bucket Algorithm

The Token Bucket is one of the oldest and most popular rate limiting algorithms. It was originally used in networking (traffic shaping) and translates cleanly to API throttling.

How It Works

A bucket holds a fixed number of tokens. Tokens are added at a constant rate, up to the bucket's capacity. Each incoming request removes one token. If the bucket is empty, the request is denied (or queued). The bucket size controls bursts: a client that has been idle accumulates tokens and can then send a burst of requests up to the bucket capacity all at once.

Pseudocode

class TokenBucket:
    capacity    // max tokens the bucket can hold
    refillRate  // tokens added per second
    tokens      // current token count
    lastRefill  // timestamp of last refill

    function allowRequest():
        now = currentTime()
        elapsed = now - lastRefill
        tokens = min(capacity, tokens + elapsed * refillRate)
        lastRefill = now

        if tokens >= 1:
            tokens = tokens - 1
            return true
        else:
            return false
    

Pros and Cons

Pros: Allows natural bursts of traffic. Memory efficient -- only a few fields per client (bucket state, last refill timestamp). Smooths out traffic patterns without hard cutoffs.

Cons: Two parameters (capacity and refill rate) mean more tuning. If clients are short-lived, maintaining per-client buckets can become expensive. Bursts can still overwhelm downstream systems if the bucket is sized too generously.

Token Bucket is a strong default for most public APIs. It is forgiving of intermittent usage patterns and does not penalize clients for periods of inactivity.

Fixed Window Algorithm

The Fixed Window algorithm is the simplest approach. It divides time into discrete windows (for example, one minute) and counts how many requests arrive in each window.

How It Works

Each time window starts at a calendar-aligned boundary (the start of the minute, the start of the hour). A counter resets at each boundary. When a request arrives, the system checks the current window's counter. If it is below the limit, the request is allowed and the counter is incremented. If it meets or exceeds the limit, the request is rejected.

Pseudocode

class FixedWindow:
    limit       // max requests per window
    windowSize  // window duration in seconds

    // Keyed by clientId: { windowStart, count }
    windows

    function allowRequest(clientId):
        now = currentTime()
        windowStart = floor(now / windowSize) * windowSize
        entry = windows[clientId]

        if entry is null or entry.windowStart != windowStart:
            windows[clientId] = { windowStart, count: 1 }
            return true

        if entry.count < limit:
            entry.count = entry.count + 1
            return true
        else:
            return false
    

Pros and Cons

Pros: Extremely simple to implement and reason about. Minimal memory -- one counter per client per window. Works well for hard quotas (e.g., "1000 requests per hour").

Cons: Suffers from the boundary problem (also called the "thundering herd" at window edges). A client can send a burst of traffic at the very end of one window and another burst at the start of the next window, effectively doubling the allowed throughput in a short span. This can be a real problem if your backend cannot handle the spike.

Fixed Window is best suited for low-stakes rate limiting where occasional spikes are acceptable, such as read-only public endpoints or internal tools.

Sliding Log Algorithm

The Sliding Log algorithm (often called Sliding Window Log) eliminates the boundary problem by tracking individual request timestamps within a sliding time window.

How It Works

For each client, maintain a sorted log of timestamps for every request. When a new request arrives, remove timestamps older than the window duration. Then check if the remaining count is below the limit. If yes, add the new timestamp and allow the request. If the limit is reached, reject the request.

Pseudocode

class SlidingLog:
    limit       // max requests per window
    windowSize  // window duration in seconds

    // Keyed by clientId: sorted list of timestamps
    logs

    function allowRequest(clientId):
        now = currentTime()
        cutoff = now - windowSize
        log = logs[clientId]

        // Remove expired entries
        while log is not empty and log[0] < cutoff:
            remove first entry from log

        if len(log) < limit:
            append now to log
            logs[clientId] = log
            return true
        else:
            return false
    

Pros and Cons

Pros: Most accurate of the three strategies. No boundary spikes -- the window slides continuously with each request. The rate limit is enforced precisely over any rolling interval.

Cons: High memory usage -- every request timestamp must be stored until it falls out of the window. For high-traffic APIs this can mean storing thousands of timestamps per active client. Can also be slower if the log data structure is not optimized (e.g., using a linked list or deque with O(1) trim).

Sliding Log is the right choice when precision matters most -- for example, billing-related endpoints where every request counts toward a hard quota, or real-time financial data APIs.

Comparison Table

Criterion Token Bucket Fixed Window Sliding Log
Implementation complexity Medium Low Medium
Memory per client Low (2-3 fields) Low (2 fields) High (all timestamps)
Burst behavior Controlled bursts (up to capacity) Spikes at window boundaries No burst spikes
Accuracy Good (smoothed) Poor at boundaries Excellent (true sliding)
Ease of tuning Two parameters Single parameter Single parameter
Best use case General-purpose APIs Simple quotas, internal tools Hard quotas, billing, real-time data

Which Strategy Should You Choose?

There is no single answer, but there are clear guidelines.

If you are building a general-purpose public API and want a forgiving experience for developers, start with Token Bucket. It handles bursty clients gracefully, is easy to understand, and has a proven track record in production. The ProfitEngine API uses rate limiting with a 60 requests per minute free tier, and a token-bucket-style approach works well for this kind of tiered offering because it allows occasional bursts (a developer testing an integration can fire off several quick calls) while still capping sustained usage.

If simplicity is your top priority and occasional spikes are acceptable, choose Fixed Window. It is trivial to implement in middleware, and many caching layers (Redis with TTL counters) map directly to this pattern. Just be aware of the boundary problem and consider whether a 2x spike at window edges is tolerable for your backend.

If precision is non-negotiable -- you are billing by the request, enforcing strict SLAs, or exposing real-time market data -- use Sliding Log. The memory cost is worth the accuracy. For very high traffic you can optimize by switching to a Sliding Window Counter (a hybrid that approximates sliding behavior using two counters), which reduces memory while still avoiding the boundary problem.

Implementing Rate Limiting in Practice

Regardless of the algorithm you choose, a few implementation patterns apply across the board.

Use a fast data store. Redis is the industry standard for rate limiting because it offers atomic operations (INCR, EXPIRE) and sub-millisecond latency. Token Bucket and Fixed Window map naturally to Redis sorted sets and counters. For Sliding Log, a Redis sorted set with ZREMRANGEBYSCORE provides an efficient O(log N) trim.

Return standard headers. Your rate limiter should set X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. This lets clients self-regulate and avoids surprises.

Communicate limits clearly. When a client is rate limited, return HTTP 429 Too Many Requests with a Retry-After header. Errors should include a clear message explaining the limit and when the client can retry.

Apply limits at the right layer. Rate limiting can live in your API gateway, a middleware layer, or directly in your application code. Gateway-level rate limiting (for example, with Kong, Envoy, or an API management platform) is easiest to manage across a microservice architecture.

Monitor and adjust. No algorithm is set-and-forget. Log rate-limited requests, track how often clients hit the limit, and watch for patterns that suggest your parameters need tuning. If most clients never come close to the limit, you may be over-restricting legitimate usage.

Conclusion

Token Bucket, Fixed Window, and Sliding Log each serve a distinct purpose. Token Bucket is the balanced choice for most public APIs. Fixed Window is the simplest but has blind spots. Sliding Log is the most precise but carries a memory cost. Match your choice to your traffic patterns, your tolerance for spikes, and how much you are willing to spend on infrastructure. And when in doubt, start with the algorithm that is easiest to switch out later -- a good rate limiter implementation hides the strategy behind a clean interface, so you can always evolve as your API grows.