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.