Ana Sayfa / Blog / API rate limiting: token bucket vs sliding window implementation

API rate limiting: token bucket vs sliding window implementation

API abuse'u önlemek ve fair-use kurmak için rate limiting şart. İki popüler algoritma ve pratik implementasyonları.

Her public API’ın bir gün rate limiting’e ihtiyacı oluyor. Bir kullanıcı script yazıp saniyede 1000 request atıyor veya bot API key’leri hammer’lıyor. Rate limiting bu abuse’u önlüyor ve fair-use sağlıyor.

İki popüler algoritma var: token bucket ve sliding window. Üçüncü bir klasik olan fixed window da mevcut ama pratikte tercih edilmiyor. Bu yazıda ikisini karşılaştırıp pragmatic implementation vereceğim.

Fixed window (en basit ama sorunlu)

Fixed window en basit. “Saat başına 100 request.” Counter tutuyorsun, saat değiştiğinde sıfırlıyorsun.

Problem: boundary case’te 2x traffic. Saat 10:59’da kullanıcı 100 request attı. Saat 11:00’da hemen 100 request daha attı. 1 dakika içinde 200 request, oysa kural saatte 100 olmalıydı.

Bu abuse burst’ler karşısında hassas.

Bu yüzden yaygın olarak kullanılmıyor. Sliding window veya token bucket tercih ediliyor.

Token bucket

Bir bucket’ın var, belirli rate’te token doluyor. Her request bir token harcıyor. Bucket boşsa request reddedildi.

Parameters:
- capacity: bucket'ın maks boyutu (burst için)
- refillRate: saniyede kaç token eklensin

Algorithm:
1. İstek geldi
2. Son refill'dan bu yana geçen süreyi hesapla
3. (geçenSüre * refillRate) token ekle (capacity'yi geçmeden)
4. Bucket >= 1 ise token harca, request'e izin ver
5. Aksi halde request reddet

Örnek: capacity=100, refillRate=10 token/sec. Bu “saniyede 10 request, burst’te 100’e kadar kabul edilir” demek.

Redis implementation:

KEYS: user:123:bucket
VALUES: {tokens: 100, lastRefill: 1710000000}

Lua script (atomic):
  local tokens, lastRefill = ...
  local now = ...
  local elapsed = now - lastRefill
  local newTokens = math.min(capacity, tokens + elapsed * refillRate)
  if newTokens >= 1 then
    return {newTokens - 1, now} -- allow
  else
    return {newTokens, now} -- deny
  end

Avantajlar:
– Burst’e toleranslı (capacity’ye kadar)
– Memory efficient (sadece {tokens, timestamp} tutuyorsun)
– Smooth rate limit (boundary issue yok)

Dezavantajlar:
– Burst kullanıcıyı unfair avantajlı yapıyor (kullandıysa ondan sonrakiler etkileniyor)
– Refill rate tuning gerektiriyor

Sliding window

Sliding window her zaman son N dakikadaki request count’unu tutuyor. Request geldiğinde sliding window’daki count’a bakıyor.

İki varyant:

Sliding window log: Her request’in timestamp’ini tut. Request gelince “son 60 saniyede kaç log var” say.

KEY: user:123:requests (Redis sorted set)
MEMBERS: [timestamp1, timestamp2, ...]

On request:
1. Şu anki timestamp = now
2. Eski timestamp'leri sil: ZREMRANGEBYSCORE key -inf (now-60)
3. Count: ZCARD key
4. If count < limit: ZADD key now now, allow
5. Else: deny

Saniyede 10 request limit: Redis’te ZCARD 10’dan büyükse deny.

Avantaj: exact calculation, fair.
Dezavantaj: memory intensive – her request log’da tutuluyor.

Sliding window counter (approximate): İki fixed window’u proportional blend et. “Şu anki minute 50% ve önceki minute’in kalan 50%’si”.

Less memory, almost exact. Pratikte yeterli.

currentMinuteCount + (previousMinuteCount * (60 - secondsIntoCurrentMinute) / 60)

Bu formülle “son 60 saniye” approximation’ı oluyor. Memory için sadece 2 counter yeterli.

Hangisini seç?

Token bucket:
– Public API (GitHub, Stripe, Twitter)
– Burst’e izin vermek istiyorsan (“user occasionally spikes, but overall kabul edilebilir”)
– Memory-constrained environment

Sliding window log:
– Exact rate tracking kritik
– Audit/debug için request history lazım
– Küçük scale (~10K users)

Sliding window counter:
– Large scale, memory matters
– Acceptable approximation (real rate vs calculated rate ~%5 farklı olabilir)
– Standard web API’leri için iyi default

Benim default tercih’im sliding window counter. Memory efficient, fair, production-ready.

Tiered rate limiting

Her API farklı limit’e sahip olmak istiyorsun. Free tier farklı, pro farklı:

function checkRateLimit(userId, endpoint) {
    const user = await getUserTier(userId);
    const limit = rateLimits[user.tier][endpoint];
    // limit: {requests: 100, per: 60}
    return await rateLimiter.check(userId, endpoint, limit);
}

Tier’ler:
– Free: 100 req/hour per user
– Pro: 1000 req/hour per user
– Enterprise: 10000 req/hour per user + higher burst

Response headers

Rate limit bilgisini client’a bildir. Standart header’lar:

X-RateLimit-Limit: 100          # per window
X-RateLimit-Remaining: 42       # kaç tane kaldı
X-RateLimit-Reset: 1710000060   # ne zaman reset olacak (Unix timestamp)
Retry-After: 30                 # (429 ile birlikte) kaç saniye bekle

Client developer’lar bu header’ları görünce kendi rate limiting logic’lerini ayarlayabiliyor. Bu zaman kaybını azaltıyor.

429 response’u

Rate limit aşıldığında 429 Too Many Requests dön:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Reset: 1710000060
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Please try again in 30 seconds.",
  "retry_after": 30
}

Client bu response’u gördüğünde retry mantığını exponential backoff ile kuruyor.

Distributed rate limiting

API server’ınız 5 instance’lı. Her instance kendi in-memory rate limiter’ını tutsa, kullanıcı 5x limit harcıyor.

Shared state için:

Redis: Çoğu setup için yeter. Low latency, atomic operations (Lua script).

Memcached: Redis’ten biraz daha hızlı ama persistent değil.

In-memory sharing (consistent hashing): Her user’ın request’i hep aynı instance’a gidiyor. Load balancer sticky routing. Complex ama low latency.

Benim default’um Redis Lua script. Atomic check-and-decrement tek operation, race condition yok.

Abuse patterns

Rate limit ederken dikkat:

IP-based rate limit’in limit’i: NAT arkasındaki multiple user aynı IP’den geliyor. 100 kullanıcılı bir şirket ofisi tek IP ile 5x rate limit’e ulaşıyor.

User-based rate limit’in limit’i: Bot’lar multiple account oluşturuyor. User başına limit abuse edilebiliyor.

Endpoint-based rate limit: Expensive endpoint’ler (large search, batch operation) ayrı limit olmalı. “Her endpoint saatte 1000” yetersiz.

Best practice: multiple rate limit layer’ı:
– Per IP: daha geniş (NAT için)
– Per user: standard
– Per endpoint: özellikle expensive olanlar

Herhangi biri aşıldığında 429.

Monitoring

Rate limit’in doğru kurulduğundan emin olmak için:

  • 429 rate: Toplam request’in kaç %’i 429. Çok yüksekse limit’ler agresif, çok düşükse limit çalışmıyor.
  • Per-user usage dağılımı: Üst %1 kullanıcı ne kadar tüketiyor?
  • Spike detection: Aynı kullanıcı aniden 10x traffic attı, suspicious.
  • Abuse patterns: Rate limit bypass denemeleri (multiple accounts, IP rotation).

Sonuç

Rate limiting API’niz production’a çıkmadan önce yapılması gereken. Token bucket ve sliding window counter pragmatic seçenekler. Redis tabanlı distributed implementation çoğu ölçek için yeterli.

Tiered limits (free vs pro), standard response header’lar, proper 429 response’u API ecosystem’ini sağlıklı kılıyor. Monitoring koy, abuse pattern’larını izle, gerekirse limit’leri ayarla.

İlk versiyonda simple counter da yeter. Ölçek büyüdükçe sophisticate etmek mümkün.

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ç