DEV Community

Cover image for Sliding vs Fixed Window Rate Limiting Performance: Which Scales Better
Amitesh0512
Amitesh0512

Posted on Originally published at amiteshsurwar.com

Sliding vs Fixed Window Rate Limiting Performance: Which Scales Better

Sliding vs Fixed Window Rate Limiting Performance: Which Scales Better

Quick Answer

Explore the Sliding Window vs Fixed Window Rate Limiting Performance trade‑offs in high‑throughput systems, with real‑world code, scaling tips, and production pitfalls.

Sliding Window vs Fixed Window Rate Limiting: The Performance Battle in Real‑World APIs

When you’re pushing a public API to 10‑plus M RPS, the limiter that once felt like a safety net becomes the single source of latency spikes and memory bloat. It’s no longer an academic question of which algorithm is “cleaner”; it’s a design decision that can make or break your SLA, your cost envelope, and even your brand trust. Below I’ll walk through a concrete problem framing, a production case study, the hard trade‑offs you’ll face, and a pragmatic decision guide that will help you choose the right window for the job.

Problem Framing

  • High‑throughput edge services (e.g., fintech API gateways) need to protect downstream services from sudden spikes while still honoring per‑minute credit limits.
  • Rate limiting is typically implemented in a distributed cache (Redis, DynamoDB, or in‑memory sharded store). The choice of algorithm directly impacts round‑trip count, memory footprint, and burst tolerance.
  • At scale, even a 0.5 ms difference per request multiplies into gigabytes of additional traffic to the cache cluster and pushes tail latency beyond acceptable thresholds.

When This Fails in Production

  • Fixed Window “Thundering Herd”: A 1‑second bucket that resets at the exact second boundary caused a 30‑minute batch job to see a 200 % spike in rejected requests, leading to SLA violations.
  • Sliding Window Over‑Provisioning: Storing raw timestamps for every request in a sorted set blew out Redis memory by 12 GB in a 30‑minute window, forcing the cluster to evict keys and introduce cache misses.
  • Mixed‑Region Latency: Using a single Redis cluster in US‑East for a global user base introduced > 50 ms round‑trip for EU users, pushing latency above the 200 ms threshold in the EU‑SLO.

Common Mistakes Engineers Make

  • Assuming INCR is always the fastest path without benchmarking under realistic traffic patterns.
  • Ignoring the memory cost of sorted sets when the window spans minutes and the burst rate is high.
  • Using a single global bucket for all tenants instead of sharding by tenant ID, which creates hot spots.
  • Over‑optimizing for the worst case by adding a second round‑trip for pruning, only to hit cache eviction due to increased memory pressure.

Real‑World Example

A fintech platform that exposes a “transfer” endpoint served 4.5 M RPS across 2,300 microservices. The original design used a sliding window with a 60‑second sorted set per customer. During a flash‑sale, a single customer’s burst of 3,000 requests in 2 seconds caused the Redis cluster to exceed 90 % memory usage. The cluster started evicting unrelated keys, leading to a cascade of 429 responses that lasted 45 seconds. The fix was a hybrid limiter: a 1‑second fixed bucket capped the absolute maximum, while a 60‑second sliding window enforced the rolling credit limit. This reduced memory usage by 85 % and eliminated the eviction spikes.

Trade‑offs

Metric Sliding Window Fixed Window
Round‑trip per request 2 (prune + add) or 3 (if using Lua) 1 (atomic INCR)
Memory per client O(N) timestamps (≈5 KB per 60 s burst) O(1) counter (≈8 bytes + overhead)
Burst tolerance Smooth decay – no hard cutoff Hard reset at bucket boundary – can reject legitimate bursts
Implementation complexity Higher – pruning logic, potential Lua scripting Lower – single counter, TTL
Consistency requirements Strong per‑request consistency (all timestamps visible) Eventual consistency acceptable if using replicas

Performance Considerations

  • Latency: Fixed window INCR typically <0.3 ms on a 10‑node Redis cluster; sliding window with pruning can reach 0.8–1.0 ms.
  • Network traffic: 0.5 ms per request translates to ~5 GB/s additional traffic at 10 M RPS.
  • CPU: Sorted set operations (ZADD + ZREM) consume ~30 % more CPU than INCR at high rates.
  • Memory: Sliding window can reach 5 GB of Redis memory for 1 M concurrent users; fixed window stays under 10 MB.
  • Observability: Fixed window counters are trivial to expose via KEYS or SCAN, while sliding windows require scanning sorted sets or maintaining auxiliary counters.

Better Approach Based on Experience

In production, I’ve found the hybrid pattern to be the most resilient:

  1. Outer Fixed Bucket: A 1‑second INCR that caps the absolute burst. Use a TTL equal to the bucket period to auto‑reset.
  2. Inner Sliding Window: A 60‑second window implemented as a bucketed counter (e.g., 1‑second sub‑buckets) that aggregates into a sorted set of bucket sums. This keeps state O(1) per sub‑bucket while preserving smoothness.
  3. Lua Scripting: Combine the two checks in a single Lua script to eliminate round‑trip overhead and ensure atomicity.
-- Redis Lua script for hybrid limiter
local key_outer = KEYS[1]
local key_inner = KEYS[2]
local now = tonumber(ARGV[1])
local period_outer = tonumber(ARGV[2])
local period_inner = tonumber(ARGV[3])
local limit_outer = tonumber(ARGV[4])
local limit_inner = tonumber(ARGV[5])

-- Outer fixed bucket
local bucket_outer = math.floor(now / period_outer)
local key_o = key_outer .. ':' .. bucket_outer
local count_o = redis.call('INCR', key_o)
if count_o == 1 then redis.call('EXPIRE', key_o, period_outer) end
if count_o > limit_outer then return 0 end

-- Inner sliding window using bucketed sums
local bucket_inner = math.floor(now / period_inner)
local key_i = key_inner .. ':' .. bucket_inner
local count_i = redis.call('INCR', key_i)
if count_i == 1 then redis.call('EXPIRE', key_i, period_inner) end

-- Sum recent buckets (e.g., last 60 buckets for 60s window)
local sum = 0
for i=0,59 do
  sum = sum + tonumber(redis.call('GET', key_inner .. ':' .. (bucket_inner - i)) or 0)
end
if sum > limit_inner then return 0 end
return 1
Enter fullscreen mode Exit fullscreen mode

This pattern gives you the burst protection of a fixed window plus the rolling fairness of a sliding window, all within a single round‑trip and with memory bounded by the number of sub‑buckets.

Decision Guide

  1. What is the SLA for latency per request? • <0.3 ms: Prefer fixed window or hybrid with Lua. • 0.3–0.7 ms: Sliding window with pruning is acceptable if burst tolerance is critical.
  2. What is the memory budget on the limiter store? • <1 GB: Fixed window. • >1 GB: Sliding window only if you bucket timestamps (e.g., per‑second buckets).
  3. Do you need true rolling quotas (e.g., per‑minute credits)? • Yes: Sliding window or hybrid. • No: Fixed window suffices.
  4. Is burst traffic common and legitimate? • Yes: Sliding window or hybrid to avoid 429 spikes. • No: Fixed window to enforce hard caps.
  5. Can you tolerate eventual consistency for the limiter? • Yes: Use replicas for reads, reduce latency. • No: Stick to the primary for strong consistency.

Scaling Notes

  • **Sharding**: Partition keys by tenant hash to avoid hot spots. For Redis, use key prefixes like rl:{tenantId}:{bucket} and ensure consistent hash slots.
  • **Global Consistency**: If you need to enforce limits across regions, use a globally replicated store (e.g., DynamoDB Global Tables) and accept a 10–30 ms replication lag. For strict compliance, keep a single primary region and route all limiter traffic through it.
  • **Circuit Breaker**: At > 10 M RPS, the limiter itself can become a bottleneck. Deploy a tiered architecture: a lightweight local cache (e.g., in‑process array) for the most active clients, backed by a global store for the rest.
  • **Observability**: Expose per‑bucket counters to Prometheus and use latency histograms to detect tail spikes early. A sudden rise in 429 rate often signals a limiter bottleneck rather than a downstream failure.

What to Ship

  • Deploy a side‑by‑side comparison of sliding and fixed window rate limiters in your API gateway, each instrumented to log per‑request latency and counter increments.
  • Add a Redis sorted‑set based sliding window implementation and a Redis atomic counter based fixed window implementation; keep the key namespaces identical so the comparison is fair.
  • Run a load test that generates 10 k QPS with burst patterns, and capture throughput, latency percentiles, and memory usage for both strategies.
  • Use the benchmark results to tune the sliding window size: if latency spikes above 200 ms, reduce the window by 25 %; if memory consumption exceeds 50 MB, increase the window size.
  • Implement a fallback that switches to the fixed window limiter when sliding window memory crosses a set threshold, ensuring graceful degradation under pressure.
  • Package the chosen limiter configuration, benchmark scripts, and a README that explains how to reproduce the performance tests in CI pipelines.

Conclusion

The choice between sliding and fixed windows is not a binary “pick one” decision; it’s a spectrum that depends on latency, memory, burst tolerance, and consistency requirements. In production, I’ve seen the hybrid pattern—an outer fixed bucket plus an inner sliding window—deliver the best of both worlds: low latency, bounded memory, and smooth throttling. The key is to benchmark under realistic traffic, monitor aggressively, and iterate on the window size and bucket granularity until the limiter behaves like a quiet guardian rather than a noisy bottleneck.

Related Articles

Top comments (0)