Approach: use a centralized Redis-backed token-bucket implemented atomically in a Lua script so multiple app instances share state. Per-feature rollout is handled by a feature toggle service (here a Redis hash) that can enable/disable or enable X% of users for gradual rollout.python
# Requires: pip install redis
import time, hashlib
import redis
REDIS = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Lua token bucket: returns 1 if allowed, 0 if denied
TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local refill_per_sec = tonumber(ARGV[2])
local capacity = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
local delta = math.max(0, now - ts)
tokens = math.min(capacity, tokens + delta * refill_per_sec)
if tokens < cost then
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("EXPIRE", key, math.ceil(capacity / refill_per_sec) + 5)
return 0
else
tokens = tokens - cost
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("EXPIRE", key, math.ceil(capacity / refill_per_sec) + 5)
return 1
end
"""
class DistributedRateLimiter:
def __init__(self, redis_conn=REDIS):
self.redis = redis_conn
self.token_bucket = self.redis.register_script(TOKEN_BUCKET_LUA)
def _feature_enabled(self, feature, key):
# feature toggles stored in Redis hash "feature:{name}"
h = self.redis.hgetall(f"feature:{feature}") # e.g. {"enabled":"1","pct":"20"}
if not h or h.get("enabled","0") != "1":
return False
pct = int(h.get("pct","100"))
if pct >= 100:
return True
# deterministic rollout: hash key into 0-99
bucket = int(hashlib.sha1(key.encode()).hexdigest(),16) % 100
return bucket < pct
def allow_request(self, key, feature, rpm):
if not self._feature_enabled(feature, key):
return False
# token bucket params
capacity = rpm # tokens max
refill_per_sec = rpm / 60.0
now = time.time()
kb = f"rl:{feature}:{key}"
allowed = self.token_bucket(keys=[kb], args=[now, refill_per_sec, capacity, 1])
return bool(int(allowed))
Key points:- Atomic updates via Lua ensure correctness across instances.- capacity=rpm gives per-minute burst allowance; refill_per_sec=rpm/60 enforces rate over time.- Feature toggles in Redis support enable/disable and percentage rollout. You can replace with LaunchDarkly/Flagsmith for richer semantics.Deployment considerations:- Use a Redis cluster with replication and failover (Redis Sentinel/Cluster) for HA. Lua runs on the node owning the key; ensure keys are not cross-sharded or use client-side sharding awareness.- Monitor Redis latency/CPU; heavy rate limiting can be hot on few keys—consider pre-sharding keys by feature or hashing to spread load.- Clock: script uses server-provided "now" from client; to avoid skew use Redis TIME or server-side time via EVALSHA with redis.call("TIME").- Security: limit Redis access, use TLS/auth.- Consistency/limits: token bucket gives soft bursts; for strict fixed windows use Redis INCR with EXPIRE (but less smooth).- Metrics/tracing: emit allowed/denied counts, latency, and feature rollout metrics.- Testing: load-test under multi-instance concurrency and simulate Redis failover.