Ana Sayfa / Blog / Distributed rate limiting: multiple server’da nasıl state paylaşılır?

Distributed rate limiting: multiple server’da nasıl state paylaşılır?

Tek server'da rate limit kolay. 5 server'lı cluster'da shared state sorunu. Redis Lua, database, consistent hashing approach'ları.

Tek bir application server’da rate limiting basit. In-memory counter tutuyorsun, her request’te kontrol ediyorsun. Ama production’da genelde 5-10 server var. Her server kendi counter’ını tutarsa kullanıcı rate limit’in 5-10 katını harcayabiliyor.

Distributed rate limiting bu shared state problem’ini çözüyor. Bu yazıda pratik yaklaşımları ve trade-off’ları anlatacağım.

Problem: cluster’da state sharing

Kullanıcı user_id=123, rate limit saniyede 10 request.

5 server’lı cluster. Load balancer request’i random server’a yolluyor. Her server kendi counter’ını tutuyor.

User aynı saniyede 50 request atıyor. Her server 10 request alıyor. Her server kendi counter’ında “user henüz 10 request yaptı” görüyor. Hepsi pass. Actual: 50 request through, rate limit %500 abused.

Fix: counter’ı shared storage’da tut.

Approach 1: Redis + Lua script

En yaygın çözüm. Redis centralized storage, Lua script atomic operation.

Implementation:

# Lua script (atomic, race condition yok)
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call('INCR', key)
if current == 1 then
    redis.call('EXPIRE', key, window)
end

if current > limit then
    return {0, current}  -- deny
else
    return {1, current}  -- allow
end

Usage:

result = redis.eval(script, keys=[f'rate:{user_id}'], args=[10, 1])
if result[0] == 0:
    return HTTPError(429)

Avantajlar:
– Atomic: race condition impossible (single-threaded Redis + Lua script)
– Low latency: ~1ms operation
– Standard pattern: production-proven
– Simple implementation

Dezavantajlar:
– Redis bottleneck: çok yüksek throughput’ta (100K+ RPS) single Redis instance yetmiyor
– Network hop: her request için Redis call
– Redis down ise rate limit çalışmıyor (fail-open vs fail-closed karar)

Çoğu production için bu yaklaşım yeterli.

Approach 2: Sliding window log (Redis sorted set)

Daha hassas rate limiting:

# Her request'in timestamp'ini kaydet
now = time.time()
redis.zadd(f'rate:{user_id}', {now: now})

# Eski timestamp'leri sil
redis.zremrangebyscore(f'rate:{user_id}', 0, now - 60)  # 60 saniyelik window

# Count
count = redis.zcard(f'rate:{user_id}')
if count > 10:
    return HTTPError(429)

Fixed window’un boundary issue’larını çözüyor. Exact count.

Dezavantaj: memory intensive. Her request log’da tutuluyor.

Kullanım: low-volume, exact-rate kritik senaryolar.

Approach 3: Token bucket (Redis-backed)

Token bucket algoritması cluster’da:

# Bucket state: {tokens, lastRefill}
# Redis hash kullanılıyor

local bucket = redis.call('HGETALL', KEYS[1])
local tokens = tonumber(bucket['tokens']) or capacity
local lastRefill = tonumber(bucket['lastRefill']) or now

-- Refill
local elapsed = now - lastRefill
local newTokens = math.min(capacity, tokens + elapsed * refillRate)

if newTokens >= 1 then
    redis.call('HMSET', KEYS[1], 'tokens', newTokens - 1, 'lastRefill', now)
    return 1  -- allow
else
    redis.call('HMSET', KEYS[1], 'tokens', newTokens, 'lastRefill', now)
    return 0  -- deny
end

Burst’e tolerans, smooth rate limit. API gateway’lerde yaygın.

Approach 4: Database-backed (not recommended)

Redis yoksa database de kullanılabiliyor:

SELECT request_count FROM rate_limits WHERE user_id = 123 AND window_start > NOW() - INTERVAL '1 minute';

Problem: Database hit per request. Latency 10-50ms. High load’da DB connection pool saturate oluyor.

Kullanım: sadece Redis mümkün değilse (very rare scenario).

Approach 5: Consistent hashing (sticky routing)

Farklı yaklaşım: shared state yok, user always same server’a routed.

Load balancer consistent hashing ile user_id’yi route ediyor. User 123 hep server 3’e gidiyor. Server 3 kendi local counter’ını tutuyor.

Avantajlar:
– No network hop for rate limit check
– Very fast
– No external dependency

Dezavantajlar:
– Server down ise user kayboluyor (rate limit reset)
– Server restart ise counter sıfırlanıyor
– Rebalance durumunda karışıklık (server added/removed)
– Load distribution uneven olabilir

Kullanım: very high throughput sistemlerde, approximate rate limiting acceptable ise.

Hybrid approach: local + global

Combine: local counter + periodic Redis sync.

Every server local counter. Her 10 saniyede bir local counter’ı Redis’e flush. Redis’ten aggregate sync.

Formula: global_count = sum of all server's local count. Yaklaşık 10 saniyelik accuracy.

Avantajlar:
– Local counter = fast
– Periodic sync = cluster-aware
– Redis down’sa local’e fallback

Dezavantajlar:
– Accurate değil (10 saniye gecikme)
– Implementation karmaşık
– Edge case’ler (server restart during window)

Kullanım: very high throughput + approximate accuracy OK.

Fail-open vs fail-closed

Redis down oldu. Rate limit decision:

Fail-open: Redis yoksa rate limit uygulama, tüm request’leri geçir. Availability priority.

Fail-closed: Redis yoksa all requests deny. Security priority.

Karar use case’e göre:
– Public API: fail-open (user service continue)
– Security-critical endpoint: fail-closed (abuse önle)

Cache downtime rate limit’i etkilemesin:

try:
    result = redis.eval(script, ...)
    return result[0] == 1  # allow/deny
except RedisError:
    logger.warning("Redis down, failing open")
    return True  # allow (fail-open)

Monitoring ile Redis downtime’ını tespit et, fix et. Fail-open uzun süre abuse’u mask ediyor.

Network overhead minimization

Her request için Redis call network cost ekliyor. Optimization’lar:

1. Connection pooling. Redis connection’ı pool’la, her request için yeni TCP bağlantısı açma.

2. Pipelining. Multiple Redis command’ı single round-trip’e topla.

3. Batch check. Request grouping: 10 request birden check edip decide. Latency biraz artıyor ama throughput iki kat.

4. Local cache (short TTL). 1-2 saniyelik cached decision. Exact değil ama performant.

Optimization’lar yüksek throughput sistemlerde gerekli. Normal throughput’ta standard approach yeterli.

Multi-region consideration

Cluster multiple region’da: US, EU, Asia. Rate limit globally mi region bazlı mı?

Global rate limit: Tüm region’lardaki request’ler tek counter’da. Redis cluster cross-region replication. Latency yüksek, complexity yüksek.

Per-region rate limit: Her region kendi Redis’i, kendi counter’ı. Global limit değil, per-region limit. User bir region’da %100 kullansa diğer region’da hâlâ fresh.

Choice: çoğu senaryo per-region yeter. User zaten single region’da. Abuse threshold region bazlı yeterli.

Monitoring

Distributed rate limiter’ı izle:

  • Redis latency: p50, p99. Ani artış = rate limit slow.
  • Rate limit hit rate: Kaç % request rate limited? Çok yüksekse limit agresif, çok düşükse çalışmıyor.
  • Redis availability: Downtime alarmı.
  • Per-user rate limit hit: Top user’lar kim, neden hit ediyorlar?

Dashboard’ta bu 4 metric var. Anomaly’ler hızlı görünsün.

Start simple, evolve

Projeye başlarken:

Phase 1: In-memory rate limiter (single server). Simple. Dev/early production OK.

Phase 2: Redis + Lua script. Multi-server. Production standard.

Phase 3: Sliding window log veya token bucket (if needed). Exact rate control.

Phase 4: Hybrid local + global (very high throughput). Rare, specific use case.

Her phase 6-12 ay sonra next’e evrim. Phase 1’den Phase 3’e atlamak premature optimization.

Sonuç

Distributed rate limiting shared state problem. Redis + Lua script %80 senaryoyu çözüyor. Sliding window log exact accuracy. Consistent hashing ultra-high-throughput için.

Karar kriterleri: accuracy seviyesi, throughput, ops complexity. Start simple (Redis + Lua), gerekirse upgrade.

Fail-open/closed karar, monitoring discipline, multi-region consideration. Bu üç detay production-ready rate limiter’ı toy demo’dan ayırıyor.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç