| name | add-rate-limiting |
| description | Add rate limiting to API endpoints |
| shortcut | rate |
Add Rate Limiting to API Endpoints
Implement production-ready rate limiting with token bucket, sliding window, or fixed window algorithms using Redis for distributed state management.
When to Use This Command
Use /add-rate-limiting when you need to:
- Protect APIs from abuse and DDoS attacks
- Enforce fair usage policies across user tiers
- Prevent resource exhaustion from runaway clients
- Comply with downstream API rate limits
- Implement freemium pricing models with usage tiers
- Control costs for expensive operations (AI inference, video processing)
DON'T use this when:
- Building internal-only APIs with trusted clients (use circuit breakers instead)
- Single-user applications (no shared resource contention)
- Already behind API gateway with built-in rate limiting (avoid double limiting)
Design Decisions
This command implements Token Bucket algorithm with Redis as the primary approach because:
- Allows burst traffic while maintaining average rate (better UX)
- Distributed state enables horizontal scaling
- Redis atomic operations prevent race conditions
- Standard algorithm with well-understood behavior
Alternative considered: Sliding Window
- More accurate rate limiting (no reset boundary issues)
- Higher Redis memory usage (stores timestamp per request)
- Slightly higher computational overhead
- Recommended for strict compliance requirements
Alternative considered: Fixed Window
- Simplest implementation (single counter)
- Burst at window boundaries (2x limit possible)
- Lower memory footprint
- Recommended only for non-critical rate limiting
Alternative considered: Leaky Bucket
- Constant output rate (smooths bursty traffic)
- Complex to explain to users
- Less common in practice
- Recommended for queuing systems, not APIs
Prerequisites
Before running this command:
- Redis server installed and accessible (standalone or cluster)
- Node.js/Python runtime for middleware implementation
- API framework that supports middleware (Express, FastAPI, etc.)
- Understanding of your API usage patterns and SLO requirements
- Monitoring infrastructure to track rate limit metrics
Implementation Process
Step 1: Choose Rate Limiting Strategy
Select algorithm based on requirements: Token Bucket for user-facing APIs, Sliding Window for strict compliance, Fixed Window for internal APIs.
Step 2: Configure Redis Connection
Set up Redis client with connection pooling, retry logic, and failover handling for high availability.
Step 3: Implement Rate Limiter Middleware
Create middleware that intercepts requests, checks Redis state, and enforces limits with proper HTTP headers.
Step 4: Define Rate Limit Tiers
Configure different limits for user segments (anonymous, free, premium, enterprise) based on business requirements.
Step 5: Add Monitoring and Alerting
Instrument rate limiter with metrics for blocked requests, Redis latency, and tier usage patterns.
Output Format
The command generates:
rate-limiter.js or rate_limiter.py - Core rate limiting middleware
redis-config.js - Redis connection configuration with failover
rate-limit-tiers.json - Tiered limit definitions
rate-limiter.test.js - Comprehensive test suite
README.md - Integration guide and configuration options
docker-compose.yml - Redis setup for local development
Code Examples
Example 1: Token Bucket Rate Limiter with Express and Redis
const Redis = require('ioredis');
class TokenBucketRateLimiter {
constructor(redisClient, options = {}) {
this.redis = redisClient;
this.defaultOptions = {
points: 100,
duration: 60,
blockDuration: 60,
keyPrefix: 'rl',
...options
};
}
async consume(identifier, points = 1, options = {}) {
const opts = { ...this.defaultOptions, ...options };
const key = `${opts.keyPrefix}:${identifier}`;
const now = Date.now();
const luaScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local ttl = tonumber(ARGV[5])
-- Get current state or initialize
local tokens = tonumber(redis.call('HGET', key, 'tokens'))
local last_refill = tonumber(redis.call('HGET', key, 'last_refill'))
if not tokens then
tokens = capacity
last_refill = now
end
-- Calculate tokens to add since last refill
local time_passed = now - last_refill
local tokens_to_add = math.floor(time_passed * refill_rate)
tokens = math.min(capacity, tokens + tokens_to_add)
last_refill = now
-- Check if we can fulfill request
if tokens >= requested then
tokens = tokens - requested
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('EXPIRE', key, ttl)
return {1, tokens, last_refill}
else
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('EXPIRE', key, ttl)
return {0, tokens, last_refill}
end
`;
refillRate = opts. / opts.;
result = ..(
luaScript,
,
key,
opts.,
refillRate,
points,
now,
opts.
);
[allowed, remaining, lastRefill] = result;
resetTime = lastRefill + (opts. * );
{
: allowed === ,
: .(remaining),
: (resetTime).(),
: allowed === ? : .((opts. * - (now - lastRefill)) / )
};
}
() {
(req, res, next) => {
{
identifier = req.?. || req.;
tier = getTier ? (req) : ;
tierConfig = .(tier);
result = .(identifier, , tierConfig);
res.({
: tierConfig.,
: result.,
: result.
});
(!result.) {
res.(, result.);
res.().({
: ,
: ,
: result.
});
}
();
} (error) {
.(, error);
();
}
};
}
() {
tiers = {
: { : , : },
: { : , : },
: { : , : },
: { : , : }
};
tiers[tier] || tiers.;
}
}
redis = ({
: process.. || ,
: process.. || ,
: .(times * , ),
:
});
rateLimiter = (redis);
app.(rateLimiter.( (req) => {
(req.?. === ) ;
(req.?. === ) ;
(req.) ;
;
}));
app.(,
rateLimiter.( ({ : , : })),
handleGenerate
);
. = ;
Example 2: Sliding Window Rate Limiter in Python with FastAPI
import time
import redis.asyncio as aioredis
from fastapi import Request, Response, HTTPException
from typing import Optional, Callable
import asyncio
class SlidingWindowRateLimiter:
def __init__(self, redis_client: aioredis.Redis, window_size: int = 60, max_requests: int = 100):
self.redis = redis_client
self.window_size = window_size
self.max_requests = max_requests
self.key_prefix = "rate_limit"
async def is_allowed(self, identifier: str, tier_config: dict = None) -> dict:
"""
Sliding window algorithm using Redis sorted set
Each request is a member with score = timestamp
"""
config = tier_config or {'max_requests': self.max_requests, 'window_size': self.window_size}
now = time.time()
window_start = now - config['window_size']
key = f"{self.key_prefix}:{identifier}"
pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, , window_start)
pipe.zcard(key)
pipe.zadd(key, {(now): now})
pipe.expire(key, config[] + )
results = pipe.execute()
request_count = results[]
request_count >= config[]:
oldest = .redis.zrange(key, , , withscores=)
oldest:
oldest_time = oldest[][]
retry_after = (config[] - (now - oldest_time)) +
:
retry_after = config[]
{
: ,
: ,
: (now + retry_after),
: retry_after
}
remaining = config[] - request_count -
reset_time = (now + config[])
{
: ,
: remaining,
: reset_time,
:
}
():
():
identifier = (request.state, , ) request.client.host
tier_config =
get_tier:
tier_config = get_tier(request)
result = .is_allowed(identifier, tier_config)
response =
result[]:
response = call_next(request)
:
response = Response(
content=,
status_code=,
media_type=
)
response.headers[] = (tier_config[] tier_config .max_requests)
response.headers[] = (result[])
response.headers[] = (result[])
result[]:
response.headers[] = (result[])
response
rate_limit_middleware
fastapi FastAPI
contextlib asynccontextmanager
():
app.state.redis = aioredis.from_url()
app.state.rate_limiter = SlidingWindowRateLimiter(app.state.redis)
app.state.redis.close()
app = FastAPI(lifespan=lifespan)
() -> :
user = (request.state, , )
user:
{: , : }
user.get() == :
{: , : }
user.get() == :
{: , : }
:
{: , : }
app.middleware()(app.state.rate_limiter.middleware(get_user_tier))
Example 3: DDoS Protection with Multi-Layer Rate Limiting
const Redis = require('ioredis');
class MultiLayerRateLimiter {
constructor(redisClient) {
this.redis = redisClient;
}
async checkLayers(req) {
const layers = [
{
name: 'ip',
identifier: req.ip,
limits: { points: 1000, duration: 60 },
priority: 'high'
},
{
name: 'user',
identifier: req.user?.id || `anon:${req.ip}`,
limits: this.getUserTierLimits(req.user),
priority: 'medium'
},
{
name: 'endpoint',
identifier: ,
: .(req.),
:
}
];
( layer layers) {
result = .(layer);
(!result.) {
{
: ,
: layer.,
...result
};
}
}
{ : };
}
() {
key = ;
now = .();
count = ..(key);
(count === ) {
..(key, layer..);
}
ttl = ..(key);
allowed = count <= layer..;
{
allowed,
: .(, layer.. - count),
: now + (ttl * ),
: allowed ? : ttl
};
}
() {
(!user) { : , : };
tiers = {
: { : , : },
: { : , : },
: { : , : }
};
tiers[user.] || tiers.;
}
() {
expensiveEndpoints = {
: { : , : },
: { : , : },
: { : , : }
};
expensiveEndpoints[path] || { : , : };
}
() {
(req, res, next) => {
{
result = .(req);
(result.) {
res.({
: result.,
: result.,
: result.
});
res.().({
: ,
: result.,
: result.,
:
});
}
();
} (error) {
.(, error);
();
}
};
}
}
. = ;
Error Handling
| Error | Cause | Solution |
|---|
| "Redis connection failed" | Redis server unreachable | Check Redis server status, verify connection string, implement connection retry |
| "Rate limiter fail-closed" | Redis timeout, middleware blocking all traffic | Implement fail-open strategy with circuit breaker pattern |
| "Inconsistent rate limits" | Clock skew across servers | Use Redis time (TIME command) instead of server time |
| "Memory exhaustion" | Too many keys, no TTL set | Always set TTL on rate limit keys, use key expiration monitoring |
| "False positives from NAT" | Multiple users behind same IP | Use authenticated user IDs when available, consider X-Forwarded-For |
Configuration Options
Rate Limit Algorithms
- Token Bucket: Best for user-facing APIs with burst allowance
- Sliding Window: Most accurate, higher memory usage
- Fixed Window: Simplest, allows boundary bursts
- Leaky Bucket: Constant rate, complex UX
Tier Definitions
{
"anonymous": { "points": 20, "duration": 60 },
"free": { "points": 100, "duration": 60 },
"premium": { "points": 1000, "duration": 60 },
"enterprise": { "points": 10000, "duration": 60 }
}
Redis Configuration
- Connection pooling: Minimum 5 connections
- Retry strategy: Exponential backoff up to 2s
- Failover: Redis Sentinel or Cluster for HA
- Persistence: AOF for rate limit state recovery
Best Practices
DO:
- Return standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)
- Implement graceful degradation (fail open on Redis failure)
- Use user ID over IP when authenticated (avoids NAT issues)
- Set TTL on all Redis keys to prevent memory leaks
- Monitor rate limiter performance (latency, block rate)
- Provide clear error messages with retry guidance
DON'T:
- Block legitimate traffic (tune limits based on real usage)
- Use client-side rate limiting only (easily bypassed)
- Forget to handle Redis connection failures (causes complete outage)
- Implement synchronous Redis calls (adds latency to every request)
- Use rate limiting as only defense against DDoS (need multiple layers)
TIPS:
- Start conservative, increase limits based on monitoring
- Use different limits for different operations (read vs write)
- Implement per-endpoint rate limits for expensive operations
- Cache tier lookups to reduce database queries
- Log rate limit violations for security analysis
- Provide upgrade paths for users hitting limits
Performance Considerations
Latency Impact
- Token bucket: 1-2ms added to request (single Redis call)
- Sliding window: 2-4ms (multiple Redis operations)
- With pipelining: <1ms for all algorithms
Redis Memory Usage
- Token bucket: ~100 bytes per user
- Sliding window: ~50 bytes per request in window
- Fixed window: ~50 bytes per user per window
Throughput
- Redis can handle 100k+ operations/second
- Use Redis Cluster for horizontal scaling
- Pipeline Redis operations when possible
- Consider local caching for extremely high throughput
Security Considerations
- DDoS Protection: Implement IP-based rate limiting as first layer
- Credential Stuffing: Add stricter limits on authentication endpoints
- API Scraping: Implement progressive delays for repeated violations
- Distributed Attacks: Use shared Redis across all API servers
- Bypass Attempts: Validate X-Forwarded-For headers, don't trust blindly
- State Consistency: Use Redis transactions to prevent race conditions
Troubleshooting
Rate Limits Not Enforced
redis-cli -h localhost -p 6379 ping
redis-cli --scan --pattern 'rl:*' | head -10
redis-cli TTL rl:user:123456
Too Many False Positives
redis-cli --scan --pattern 'rl:ip:*' | xargs redis-cli MGET
Redis Memory Issues
redis-cli INFO memory
redis-cli --scan --pattern 'rl:*' | wc -l
redis-cli --scan --pattern 'rl:*' | xargs redis-cli TTL | grep -c "^-1"
Related Commands
/create-monitoring - Monitor rate limit metrics and violations
/api-authentication-builder - Integrate with auth for user-based limits
/api-load-tester - Test rate limiter under realistic load
/setup-logging - Log rate limit violations for analysis
Version History
- v1.0.0 (2024-10): Initial implementation with token bucket and sliding window
- Planned v1.1.0: Add adaptive rate limiting based on system load