Why Fixed Window Rate Limiting Is Broken (And How Sliding Window Fixes It)
Most rate-limiting tutorials start with the same algorithm:
Why Fixed Window Rate Limiting Is Broken (And How Sliding Window Fixes It)
Most rate-limiting tutorials start with the same algorithm:

- Allow 100 requests per minute
- Store a counter in Redis
- Increment it on every request
- Reset it every 60 seconds
Simple. Fast. Easy to implement. And surprisingly flawed.
Under real traffic conditions, a user can often send nearly twice the intended limit without violating the algorithm.
A “100 requests per minute” limiter can effectively become a “200 requests per minute” limiter.
This isn’t a bug in your implementation, It’s a weakness in the algorithm itself.
While building RedisWall, an open-source distributed rate limiter for Node.js and Redis, I implemented multiple rate-limiting strategies and quickly discovered why many production systems avoid fixed windows entirely.
Let’s see why.
How Fixed Window Rate Limiting Works
Fixed window rate limiting divides time into discrete buckets. For a limit of 100 requests per minute:
const windowId = Math.floor(Date.now() / 60000);
const key = `user:${userId}:${windowId}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, 60);
}
return count <= 100;
Each minute gets its own Redis key:
user:123:28653451
user:123:28653452
user:123:28653453
When the minute changes, a new key is used and the counter starts from zero again.
The advantages are obvious:
- Extremely simple
- Constant memory usage
- One Redis counter per user
- Very fast
That’s why fixed windows are still one of the most common rate-limiting approaches. Unfortunately, they contain a loophole.
The Cross-Boundary Burst Problem
Imagine a limit of:
100 requests per minute
Now consider this traffic pattern:
00:59.900 → 100 requests
01:00.100 → 100 requests
Only 200 milliseconds passed.
But the requests landed in different windows.
Window A
00:00 → 00:59
Receives:
100 requests
Allowed.
Window B
01:00 → 01:59
Receives:
100 requests
Also allowed.
The rate limiter sees:
Window A = 100
Window B = 100
Both are within the limit. But reality looks very different. The server just received:
200 requests in 200 milliseconds
Even though the intended limit was:
100 requests per minute
The user effectively doubled the allowed rate simply by timing requests around a window boundary.
This is known as the cross-boundary burst problem.
Why This Actually Matters
At first glance this may seem harmless. Who cares if someone gets a few extra requests? The problem becomes serious when rate limiting exists to protect expensive resources.
Examples include:
- Databases
- Payment processors
- AI APIs
- Authentication systems
- Third-party integrations
- Webhook consumers
Imagine your infrastructure was sized for:
100 requests per minute
But a burst suddenly becomes:
200 requests in less than a second
That burst can:
- Exhaust database connection pools
- Increase latency
- Trigger downstream rate limits
- Cause cascading failures
- Increase infrastructure costs
The closer your system operates to its limits, the more dangerous these bursts become.
The algorithm is enforcing the rule incorrectly.
The Core Problem
The issue isn’t Redis. The issue isn’t implementation quality. The issue is how fixed windows think about time.
Fixed windows treat time as a collection of separate buckets:
Minute 1
Minute 2
Minute 3
Minute 4
Once a bucket ends, the algorithm forgets everything that happened inside it. Requests that occurred one millisecond ago become completely irrelevant after a window reset. That’s the fundamental flaw.
How Sliding Window Rate Limiting Works
Sliding windows approach the problem differently.
Instead of asking: How many requests happened in this bucket?
They ask: How many requests happened during the last 60 seconds?
Time becomes continuous rather than segmented.
Suppose the current time is:
12:01:30
A sliding window simply looks back:
12:00:30 → 12:01:30
and counts every request that occurred during that period. No artificial boundaries exist. No reset moments exist. No bucket transitions exist. Only the last 60 seconds matter.

Implementing Sliding Windows with Redis
A common implementation uses Redis Sorted Sets. Each request timestamp becomes a member in the set.
const now = Date.now();
await redis.zremrangebyscore(
key,
0,
now - windowMs
);
await redis.zadd(
key,
now,
`${now}-${Math.random()}`
);
const count = await redis.zcard(key);
return count <= limit;
Here’s what happens:
Step 1: Remove Old Requests
zremrangebyscore(...)
Deletes timestamps older than the window.
Step 2: Record Current Request
zadd(...)
Stores the current timestamp.
Step 3: Count Active Requests
zcard(...)
Counts requests that still fall inside the active window.
The result is a continuously moving window of activity.
Why Sliding Windows Fix the Problem
Let’s revisit the same attack.
00:59.900 → 100 requests
01:00.100 → 100 requests
The sliding window doesn’t care about minute boundaries.
When processing the second burst, it looks back 60 seconds and sees:
200 requests
Not:
100 + 100
Simply:
200
If the limit is:
100 requests
Then:
200 > 100
The requests are blocked.
The exploit disappears entirely.

The Tradeoff
Nothing is free. Sliding windows improve accuracy, but they cost more.
Fixed Window
Pros:
- Very fast
- Constant memory
- Simple implementation
Cons:
- Cross-boundary bursts
- Poor accuracy
- Can exceed intended limits
Sliding Window
Pros:
- Precise rate limiting
- No burst exploit
- Better protection for downstream services
Cons:
- More Redis operations
- Stores timestamps
- Higher memory usage
In complexity terms: MemoryFixed Window O(1) Sliding WindowO(n)
Where n is the number of requests currently inside the active window.
Which One Should You Use?
For internal systems where occasional bursts are acceptable, fixed windows may be perfectly fine.
For public APIs, authentication endpoints, AI products, payment systems, and other resource-sensitive services, sliding windows are usually the better choice.
Most developers think rate limiting is about counting requests.
In reality, it’s about accurately modeling time.
Fixed windows simplify time into buckets.
Sliding windows model time continuously.
That small difference is what eliminates the exploit.
What I Built in RedisWall
While building RedisWall, I implemented both fixed window and sliding window strategies.
The fixed window strategy exists because it’s still useful when memory efficiency is the primary concern.
The sliding window strategy exists because many production APIs care more about correctness than a few additional Redis operations.
createRedisWall({
redis,
strategy: "sliding-window",
});
Choosing the algorithm becomes a configuration decision rather than an architectural rewrite.
Final Thoughts
Fixed window rate limiting isn’t broken because it crashes.
It’s broken because it doesn’t actually enforce the limit developers think they’re enforcing.
A “100 requests per minute” rule should mean exactly that.
With fixed windows, it often doesn’t.
Sliding windows solve the problem by treating time as a continuously moving interval rather than a collection of independent buckets.
The cost is slightly more memory and a few extra Redis operations.
For most modern APIs, that’s a tradeoff worth making.
In the next article, we’ll build a distributed rate limiter from scratch using Redis, Lua scripts, and Node.js, and explore how production systems handle rate limiting at scale.
메타데이터
- post_id
- 6b16ba139025
- slug
- why-fixed-window-rate-limiting-is-broken-and-how-sliding-window-fixes-it-6b16ba139025
- url
- https://medium.com/@nakshatrathange/why-fixed-window-rate-limiting-is-broken-and-how-sliding-window-fixes-it-6b16ba139025
- canonical_url
- https://medium.com/@nakshatrathange/why-fixed-window-rate-limiting-is-broken-and-how-sliding-window-fixes-it-6b16ba139025
- author_url
- https://medium.com/@nakshatrathange
- status
- ok
- fetched_at
- 2026-09-13 21:47:39