Rate Limiting
Shared Redis counters enforce API rate limits across all pods — without central store each instance allows full quota and attackers multiply allowance by replica count.
Introduction
Shared Redis counters enforce API rate limits across all pods — without central store each instance allows full quota and attackers multiply allowance by replica count. Sliding window via ZSET timestamps or fixed window INCR+EXPIRE are common patterns.
Lua scripts make check-and-increment atomic. Return 429 with Retry-After when limit exceeded; log metric for abuse detection.
Rate limit keys per userId, API key, or IP — tier limits by subscription plan in key suffix.
Understanding the topic
Key concepts
- Fixed window: INCR key EXPIRE window.
- Sliding window: ZSET of request timestamps.
- Token bucket via Lua periodic refill.
- Key: ratelimit:{tier}:{id}:{window}.
- Atomic increment in Lua or MULTI.
- Fail open vs closed on Redis outage policy.
Step-by-step explanation
- Request arrives; compute limit key.
- Lua or INCR checks count vs threshold.
- Under limit — increment and allow.
- Over limit — reject 429.
- TTL expires window automatically.
Syntax reference
Common commands
- Fixed window burst at edges — sliding smoother.
- Lua combines INCR+EXPIRE atomically.
- Document fail-open policy for payments vs fail-closed for auth.
# Fixed window 100 req/minINCR ratelimit:user:42:202607011045EXPIRE ratelimit:user:42:202607011045 60# if count > 100 → 429
Informative example
Sliding window rate limiter with Lua in Spring service:
@Servicepublic class RateLimiter {private final StringRedisTemplate redis;public RateLimiter(StringRedisTemplate redis) {this.redis = redis;}private static final String SCRIPT = """local key = KEYS[1]local limit = tonumber(ARGV[1])local window = tonumber(ARGV[2])local now = tonumber(ARGV[3])redis.call('ZREMRANGEBYSCORE', key, 0, now - window)if redis.call('ZCARD', key) >= limit then return 0 endredis.call('ZADD', key, now, now .. ':' .. ARGV[4])redis.call('EXPIRE', key, window)return 1""";public boolean allow(String userId) {Long ok = redis.execute(RedisScript.of(SCRIPT, Long.class),List.of("rl:" + userId), "100", "60", String.valueOf(System.currentTimeMillis()), UUID.randomUUID().toString());return ok != null && ok == 1;}}
Return 429 in controller when allow false. Add metric tag userId hash for dashboards.
Real-world use
Real-world use cases
- Public API 1000 req/hour per key.
- Login brute-force throttle per IP.
- SMS OTP send limit per phone.
- GraphQL query cost limiter.
- Webhook receiver protection.
Best practices
- Lua for atomic check-increment.
- Return Retry-After header.
- Tier limits in key structure.
- Monitor 429 rate and top offenders.
- Fail closed for auth; consider fail open for non-critical reads.
- Combine with WAF edge limits.
Common mistakes
- In-memory Guava limiter per pod only.
- Fixed window double burst at boundary.
- No TTL on counter keys.
- Rate limiting after expensive work not before.
Advanced interview questions
Q1BeginnerWhy Redis for rate limiting?
Q2BeginnerINCR rate limit pattern?
Q3IntermediateFixed vs sliding window?
Q4IntermediateRedis down — allow or deny?
Q5AdvancedDesign API rate limit for 1M developers?
Summary
Central Redis counters enforce global limits.