Redis Rate Limiting in Production: Fixed Window, Sliding Window, and Token Bucket
Rate limiting is not just “count requests and return 429.” A production limiter has to answer four design questions:
- Who or what is being limited?
- Which algorithm matches the traffic pattern?
- Can concurrent API instances make one atomic allow/deny decision?
- What happens when Redis or the limiter itself is unavailable?
Redis is a strong fit because distributed API instances can share low-latency counters and state instead of keeping separate per-process limits that clients can bypass through a load balancer.
This guide focuses specifically on rate-limiting algorithms and production behavior. Broader Redis-backed API protections such as idempotency, replay defense, token revocation, and temporary abuse flags belong to a separate concern.
Choose the identity before the algorithm
A perfect token bucket is still wrong if it limits the wrong principal.
Common dimensions include:
IP address
authenticated user ID
API key ID
tenant/account ID
endpoint or business operation
resource ID
provider/model ID for expensive downstream APIs
For example:
rate:user:42:search
rate:tenant:7:export
rate:apikey:ak_123:llm_generate
Use IP-based limits carefully. They are useful before authentication and for broad abuse controls, but many real users can share one public IP through NAT, corporate gateways, or mobile networks.
For authenticated APIs, a layered policy often works better:
coarse IP protection
+ per-user/API-key quota
+ stricter limit on expensive or sensitive operations
Do not put raw bearer tokens or API secrets into Redis key names. Use stable internal identifiers instead.
The main algorithms
Redis' current rate-limiter documentation covers several common designs. The most useful starting comparison is:
| Algorithm | State cost | Burst behavior | Accuracy | Good fit |
|---|---|---|---|---|
| Fixed window | Very low | Boundary burst possible | Window-based | Simple quotas |
| Sliding window log | Grows with requests in window | Smooth | Exact rolling window | High-value, moderate-volume limits |
| Sliding window counter | Low | Smoother than fixed window | Approximate | General-purpose APIs |
| Token bucket | Low | Controlled bursts | Exact when implemented atomically | Bursty APIs with sustained-rate control |
| Leaky bucket / strict policing | Low | Little or no burst allowance | Exact when implemented atomically | Strict pacing |
There is no universally “best” limiter. Choose the failure and burst behavior the endpoint actually needs.
1. Fixed window: simplest and cheapest
A fixed window might enforce:
100 requests per 60 seconds
All requests in one minute increment the same counter. When the window expires, Redis removes the key and the next window starts from zero.
A compact atomic Lua implementation is:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local count = redis.call('INCR', key)
if count == 1 then
redis.call('EXPIRE', key, window)
end
local ttl = redis.call('PTTL', key)
if count > limit then
return {0, count, ttl}
end
return {1, count, ttl}
Redis guarantees atomic execution of Lua scripts, so concurrent application servers cannot interleave the read/update decision inside the script. The Redis scripting documentation also warns that scripts block other Redis command execution while running, so limiter scripts should stay tiny and bounded.
The boundary-burst problem
Suppose the limit is 100 requests per minute.
A client can send:
100 requests at 12:00:59
+ 100 requests at 12:01:00
Both windows are individually valid, yet nearly 200 requests can arrive in roughly one second around the boundary.
That is why fixed windows are good for simple quotas but poor when bursts themselves are dangerous.
Redis 8.8+: INCREX
Redis 8.8 introduced INCREX, which can combine incrementing, expiration, and bounds in one native command. For deployments that have confirmed Redis 8.8+ compatibility, it can simplify bounded window counters.
Do not silently depend on it if your production fleet still runs an older Redis version; the Lua/transaction patterns remain more portable.
2. Sliding window log: precise rolling limits
A sliding log stores one entry per accepted/request attempt inside the active window, typically in a sorted set:
score = request timestamp
member = unique request identifier
Each decision performs, atomically:
- remove entries older than the window;
- count the remaining entries;
- reject if the limit is reached;
- otherwise add the current request;
- refresh a bounded TTL.
Conceptually:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local member = ARGV[3]
local t = redis.call('TIME')
local now = tonumber(t[1]) + tonumber(t[2]) / 1000000
local cutoff = now - window
redis.call('ZREMRANGEBYSCORE', key, '-inf', cutoff)
local count = redis.call('ZCARD', key)
if count >= limit then
return {0, count}
end
redis.call('ZADD', key, now, member)
redis.call('EXPIRE', key, window * 2)
return {1, count + 1}
Redis' sorted-set documentation explicitly lists sliding-window rate limiting as a use case.
Using Redis TIME inside the atomic operation also avoids application-server clock differences affecting the same distributed limiter.
Trade-off: memory grows with traffic
A true sliding log keeps an entry for every request still inside the window.
If you have:
millions of principals
× high request rates
× long windows
memory and sorted-set operation cost can become significant.
Use this algorithm when precise rolling behavior is worth that state cost—not as the default for every public endpoint.
3. Sliding window counter: a practical compromise
A sliding counter stores counts for the current and previous fixed windows, then weights the previous window based on how far the current window has progressed.
Conceptually:
estimated usage =
current_count
+ previous_count × remaining_fraction_of_previous_window
This smooths the fixed-window boundary without storing one timestamp per request.
It is attractive when you need:
- low Redis memory per principal;
- smoother limits than a fixed window;
- more predictable behavior at large cardinality;
- no requirement for an exact per-request rolling audit log.
The trade-off is that the result is an estimate, not the exact set of requests that occurred during the last N seconds.
If your implementation touches both the current and previous counter keys atomically in Redis Cluster, keep them in the same hash slot—for example by sharing a hash tag such as {user:42}. Redis Cluster requires the keys involved in a multi-key transaction or Lua operation to belong to the same slot. See the official Redis Cluster scaling documentation.
4. Token bucket: allow useful bursts without unlimited spikes
Token bucket is often the best fit for APIs where occasional bursts are acceptable but sustained traffic must stay within a configured rate.
The bucket has two important values:
capacity = maximum burst size
refill rate = tokens added over time
Each request consumes one or more tokens. If enough tokens exist, allow the request. Otherwise reject it and calculate when enough capacity will be available again.
Example policy:
capacity: 20 tokens
refill: 5 tokens/second
cost: 1 token/request
This allows a short burst up to the bucket capacity while still controlling the long-term rate.
A Redis implementation commonly stores:
tokens
last_refill_time
and performs refill + consume atomically in a short Lua script or Redis Function.
The current Redis rate-limiter guides provide token-bucket examples across several official client integrations, including Node.js, Go, Python, Java, Rust, PHP, Ruby, and others.
Weighted token costs are useful
Not all API operations cost the same.
Instead of:
every request costs 1
you may intentionally model:
metadata read -> 1 token
heavy export -> 20 tokens
LLM generation -> cost based on your quota model
bulk write -> 10 tokens
That protects the resource you actually care about rather than treating a cheap health check and an expensive export as identical work.
Keep the cost model understandable and observable; complex hidden weights are difficult for API consumers and operators to debug.
Atomicity is non-negotiable
A broken distributed limiter often looks like this:
GET current_count
if current_count < limit:
INCR current_count
Two servers can read the same old count and both decide to allow a request.
Prefer primitives that make the decision atomic:
INCRwhere a single counter is enough;SET ... NXfor one-time claims;- Redis transactions when they fit;
- Lua scripts or Redis Functions for read-decide-update logic;
INCREXon Redis 8.8+ for compatible window-counter cases.
Redis Functions became available in Redis 7 and provide server-managed programmable logic when you prefer that lifecycle over application-managed EVAL scripts.
Do not make limiter scripts large. Atomicity is useful precisely because Redis executes the script as one unit; a slow script can block other clients.
Return useful HTTP behavior
When a request exceeds the allowed rate, HTTP 429 Too Many Requests is the standard response code defined by RFC 6585.
A server may also send Retry-After so the client knows when to try again:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{"error":"rate_limit_exceeded"}
Do not automatically retry a 429 immediately; that creates more load at exactly the wrong time.
What about RateLimit headers?
As of September 2026, the IETF HTTPAPI work defining RateLimit and RateLimit-Policy is still an Internet-Draft, not a published RFC.
You can adopt the current draft format when it fits your API ecosystem, but document that contract explicitly and do not describe older X-RateLimit-* headers—or the current draft—as universally standardized fields.
Limit business operations, not only URLs
A global request cap can miss the real bottleneck.
For example, these requests can have very different cost:
GET /profile
POST /send-otp
POST /reports/export
POST /ai/generate
POST /payments/confirm
Build keys around the protected operation:
rl:user:42:send-otp
rl:tenant:7:export
rl:apikey:123:ai-generate
This also lets different operations use different algorithms:
- login attempts: strict/sliding rule;
- normal API reads: token bucket;
- monthly plan quota: durable usage accounting plus a quota layer;
- burst-sensitive downstream service: smaller bucket capacity;
- expensive export: per-tenant concurrency + rate limit.
Rate limiting is one control; it should not be forced to replace concurrency limits, billing quotas, authorization, or fraud controls.
Rate limiting is not the same as concurrency limiting
A client can obey “60 requests per minute” and still launch 60 expensive 30-second operations simultaneously.
If the protected resource has a concurrency bound, track that separately.
Examples:
maximum 3 active exports per tenant
maximum 10 concurrent model calls per API key
maximum 2 active report builds per user
Use a proper lease/semaphore design with crash recovery if concurrency state must survive worker failures. A time-window rate limiter alone cannot express “how many operations are running right now?”
Redis Cluster key design
Most limiter decisions should ideally touch one logical principal key at a time. That distributes naturally across Redis Cluster hash slots.
If an algorithm requires several keys in one atomic script or transaction, Redis Cluster normally requires those keys to share a slot.
For example:
rl:{tenant-7}:current
rl:{tenant-7}:previous
The {tenant-7} hash tag keeps both keys together.
Use hash tags only when atomic co-location is required. If you put every customer in a global {ratelimit} hash tag, you can create a single hot shard and defeat horizontal distribution.
Eviction can silently weaken your limiter
Rate-limit state is not the same as disposable cache data.
Redis supports policies such as allkeys-lru that may evict keys when memory reaches maxmemory. If a limiter key disappears early, that principal may effectively receive a fresh quota.
For security- or cost-critical limits:
- size memory for expected key cardinality;
- monitor evictions;
- consider isolating limiter state from a cache whose policy intentionally evicts arbitrary keys;
- choose an eviction policy intentionally rather than inheriting one unnoticed;
- understand that
noevictionprotects existing keys from eviction but causes writes that need more memory to fail when the limit is reached.
The official Redis key-eviction documentation describes these behaviors in detail.
A limiter that “fails” by silently losing its counters is usually worse than a visible error with a defined fallback policy.
Decide what happens when Redis is unavailable
Because Redis sits on the synchronous request path, every limiter needs an outage policy.
Possible choices include:
| Endpoint | Example posture |
|---|---|
| Public low-cost read | Temporarily fail open with local emergency protection |
| Expensive third-party API | Fail closed or enforce a conservative local fallback |
| Login / OTP abuse control | Usually prefer protection over unrestricted attempts |
| Internal non-critical API | May prioritize availability |
| Paid quota enforcement | Avoid bypassing cost/account limits silently |
There is no one correct fail-open/fail-closed choice for the entire platform.
Make it an explicit per-policy setting, emit metrics when fallback mode activates, and test the Redis-down path before an incident.
Multi-region limits are a different problem
A single-region Redis limiter is straightforward: every API instance consults the same authoritative state.
Global active-active traffic introduces latency and consistency trade-offs. A request can arrive in two regions nearly simultaneously, and strict global quotas may require coordination that adds latency or sacrifices availability during partitions.
Before building a global limiter, decide whether the requirement is:
- a strict global cap;
- a slightly approximate global cap;
- independent per-region budgets that add up to the global allowance;
- a local burst limit plus durable central billing/quota accounting.
Do not label an eventually reconciled counter “strict” unless its consistency model actually guarantees that behavior under concurrent cross-region traffic.
Observe the limiter itself
Useful metrics include:
- allowed and denied requests by policy;
- limiter decision latency;
- remaining quota distribution;
- Redis errors/timeouts;
- fallback/fail-open activations;
- key count and memory by limiter namespace;
- evictions;
- hottest principals/routes;
- 429 rate by client version;
- downstream saturation before and after limiting.
A sudden increase in 429s can mean abuse—but it can also mean a broken client release, a new traffic pattern, or a policy configured too aggressively.
Production checklist
Before shipping a Redis-backed rate limiter, verify:
- the limited identity and operation are explicit;
- the algorithm matches acceptable burst behavior;
- allow/deny state changes are atomic under concurrency;
- limiter keys expire intentionally;
- high-cardinality sliding logs are memory-budgeted;
- multi-key Cluster operations use compatible hash slots;
- hash tags do not create one artificial hot shard;
-
429responses andRetry-Afterbehavior are documented; - draft
RateLimitheaders are not misrepresented as a finalized RFC; - expensive operations also have concurrency controls where needed;
- eviction cannot silently reset critical quotas;
- Redis outage behavior is defined per policy;
- multi-region consistency requirements are stated honestly;
- limiter latency, denials, Redis failures, cardinality, and evictions are monitored.
The main takeaway
A production rate limiter is a distributed policy engine, not just an INCR command.
Use fixed windows when simplicity matters, sliding windows when rolling accuracy matters, and token buckets when you want controlled bursts plus a sustained rate. Keep the decision atomic, scope it to the correct principal and business operation, return useful HTTP feedback, and plan for Cluster behavior, memory pressure, and Redis outages.
The algorithm is only half the design. The other half is making sure the limiter behaves predictably when traffic, concurrency, infrastructure, and failures stop being ideal.
BUILDING A SAAS PRODUCT?
Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.
EXPLORE SAAS SERVICES →