API Rate Limiting: Kota Yönetimi ve Throttling Rehberi
Üretim ortamında bir API’niz varsa ve hiç rate limiting yapmadıysanız, er ya da geç acı bir sürprizle karşılaşacaksınız. Ya bir istemci kontrolden çıkıp sunucunuzu felç edecek, ya bir bot sisteminizi tarayacak, ya da meşru bir kullanıcının aşırı isteği diğerlerinin deneyimini mahvedecek. Rate limiting, bu senaryoların hepsini önleyen temel bir savunma mekanizmasıdır. Bu yazıda hem teorik temeli hem de gerçek dünya uygulamalarını ele alacağız.
Rate Limiting Neden Bu Kadar Önemli?
Bir API’yi açık bırakmak, kapısı olmayan bir dükkan açmak gibidir. Çoğu müşteri düzgün davranır, ama bir tanesi tüm rafları boşaltabilir. Rate limiting sadece güvenlik meselesi değil, aynı zamanda kaynak yönetimi ve adil kullanım meselesidir.
Gerçek dünyadan bir senaryo düşünelim: E-ticaret platformunuzun ürün arama API’si var. Black Friday’de bir müşterinin geliştirici ekibi, fiyat karşılaştırma aracı için saniyede 500 istek atıyor. Sunucularınız bunu kaldıramıyor ve tüm platformunuz çöküyor. Bu tam olarak yaşandı, ve yaşanmaya da devam ediyor.
Rate limiting’in çözdüğü başlıca problemler şunlardır:
- DDoS koruması: Aşırı istek yükünü absorbe eder veya reddeder
- Adil kullanım: Bir kullanıcının diğerlerinin kotasını yemesini önler
- Maliyet kontrolü: Backend servislerinize ve veritabanlarınıza gereksiz yük binmesini engeller
- İş modeli desteği: Ücretsiz/premium tier ayrımı yapmanızı sağlar
- Bot engelleme: Otomatik tarayıcı ve scraper’ları yavaşlatır
Rate Limiting Algoritmaları
Hangi algoritmayı kullanacağınıza karar vermeden önce her birinin nasıl çalıştığını anlamak gerekiyor.
Token Bucket (Token Kovası)
En yaygın kullanılan algoritmadır. Bir kova düşünün, içinde tokenlar var. Her istek bir token tüketir. Kova boşalınca istekler reddedilir. Kova zamanla dolmaya devam eder. Bu sayede burst trafiğe izin verirken ortalama hızı sınırlarsınız.
# Redis ile basit token bucket implementasyonu
# Lua script ile atomik işlem
redis-cli EVAL "
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local last_time = tonumber(redis.call('hget', key, 'last_time') or now)
local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)
local elapsed = now - last_time
local new_tokens = math.min(capacity, tokens + elapsed * refill_rate)
if new_tokens >= requested then
redis.call('hset', key, 'tokens', new_tokens - requested)
redis.call('hset', key, 'last_time', now)
redis.call('expire', key, 3600)
return 1
else
return 0
end
" 1 "user:123:bucket" 100 10 1699000000 1
Sliding Window (Kayan Pencere)
Sabit pencere yerine gerçek zamanlı pencere kullanır. Son N saniyedeki istek sayısını takip eder. Daha adil ve kenar durumları (window edge cases) sorununu çözer.
# Redis Sorted Set ile sliding window
# Son 60 saniyedeki istek sayısını kontrol et
redis-cli MULTI
redis-cli ZREMRANGEBYSCORE "user:123:requests" 0 $(($(date +%s) - 60))
redis-cli ZCARD "user:123:requests"
redis-cli ZADD "user:123:requests" $(date +%s%3N) $(uuidgen)
redis-cli EXPIRE "user:123:requests" 120
redis-cli EXEC
Fixed Window (Sabit Pencere)
En basit algoritmadır. Her dakika/saat başında sayaç sıfırlanır. Implementasyonu kolaydır ama pencere kenarlarında burst problemi yaşanabilir.
# Basit sayaç yaklaşımı
WINDOW_KEY="ratelimit:user:123:$(date +%Y%m%d%H%M)"
CURRENT=$(redis-cli INCR "$WINDOW_KEY")
redis-cli EXPIRE "$WINDOW_KEY" 120
if [ "$CURRENT" -gt 100 ]; then
echo "Rate limit aşıldı"
else
echo "İstek kabul edildi: $CURRENT/100"
fi
Leaky Bucket (Sızdıran Kova)
Gelen istekleri bir kuyruğa alır ve sabit hızda işler. Çıktı hızını tamamen düzleştirir. WebSocket veya streaming senaryolarında idealdir.
Nginx ile Rate Limiting
Nginx, üretim ortamında rate limiting için en yaygın tercihlerden biridir. ngx_http_limit_req_module ve ngx_http_limit_conn_module modülleri bu işi yapar.
# /etc/nginx/nginx.conf veya /etc/nginx/conf.d/rate-limit.conf
# İstek hızı limiti tanımla - IP başına saniyede 10 istek
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# Bağlantı limiti - IP başına max 20 eşzamanlı bağlantı
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# API key bazlı limit (Header'dan okuma)
map $http_x_api_key $api_client {
default "anonymous";
"premium-key-abc123" "premium";
"basic-key-xyz789" "basic";
}
limit_req_zone $api_client zone=api_key_limit:10m rate=100r/s;
server {
listen 443 ssl;
server_name api.yourapp.com;
location /api/v1/ {
# Burst: 20 istek kuyruğa alınabilir, nodelay ile anında işlenir
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 20;
# Rate limit aşıldığında 429 döndür (varsayılan 503)
limit_req_status 429;
limit_conn_status 429;
# Başlıkları ekle
add_header X-RateLimit-Limit 10;
add_header Retry-After 1;
proxy_pass http://backend_api;
}
# Premium kullanıcılar için ayrı endpoint
location /api/v1/premium/ {
limit_req zone=api_key_limit burst=200 nodelay;
proxy_pass http://backend_api;
}
}
Nginx log’larından rate limit ihlallerini analiz etmek için şu komutları kullanabilirsiniz:
# Rate limit ile reddedilen istekleri say
grep "limiting requests" /var/log/nginx/error.log |
awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
# Son 1 saatte rate limit yiyen IP'leri listele
grep "$(date -d '1 hour ago' '+%d/%b/%Y:%H')" /var/log/nginx/access.log |
grep " 429 " | awk '{print $1}' | sort | uniq -c | sort -rn
# Gerçek zamanlı rate limit monitoring
tail -f /var/log/nginx/error.log | grep --line-buffered "limiting"
HAProxy ile Gelişmiş Throttling
HAProxy, daha granüler kontrol gerektiğinde güçlü bir alternatiftir. Stick table’lar sayesinde durum bilgisini saklayabilir.
# /etc/haproxy/haproxy.cfg
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend api_frontend
bind *:80
bind *:443 ssl crt /etc/ssl/api.pem
# Stick table: IP başına istek sayacı, 10 dakika TTL
stick-table type ip size 1m expire 10m store http_req_rate(60s),conn_cur,conn_rate(10s)
# Mevcut istek hızını çek
http-request track-sc0 src
# Dakikada 300'den fazla istek = engelle
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 300 }
# Eşzamanlı bağlantı limiti
http-request deny deny_status 429 if { sc_conn_cur(0) gt 50 }
# Bot tespiti: çok hızlı bağlantı açıyorsa
http-request deny deny_status 429 if { sc_conn_rate(0) gt 20 }
default_backend api_backend
backend api_backend
balance roundrobin
server api1 127.0.0.1:8080 check
server api2 127.0.0.1:8081 check
Uygulama Katmanında Rate Limiting
Altyapı seviyesindeki limitler yeterli değildir. İş mantığınıza göre daha akıllı kurallar için uygulama katmanında da rate limiting yapmanız gerekir. Aşağıdaki örnek Node.js için express-rate-limit ve ioredis kombinasyonunu göstermektedir, ancak mantık dil bağımsızdır.
Önce Redis tabanlı bir rate limiter servisi kuralım:
# Redis rate limiter için Lua script dosyası
# /opt/scripts/rate_limiter.lua
cat > /opt/scripts/rate_limiter.lua << 'EOF'
local key = KEYS[1]
local window = tonumber(ARGV[1]) -- Saniye cinsinden pencere
local limit = tonumber(ARGV[2]) -- Maksimum istek sayısı
local now = tonumber(ARGV[3]) -- Şimdiki zaman (ms)
-- Eski kayıtları temizle
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
-- Mevcut sayıyı al
local count = redis.call('ZCARD', key)
if count < limit then
-- Yeni isteği kaydet
redis.call('ZADD', key, now, now .. math.random())
redis.call('PEXPIRE', key, window * 1000)
return {1, limit - count - 1, 0}
else
-- En eski kaydın ne zaman expire olacağını hesapla
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
local reset_after = math.ceil((tonumber(oldest[2]) + window * 1000 - now) / 1000)
return {0, 0, reset_after}
end
EOF
echo "Lua script hazır"
redis-cli SCRIPT LOAD "$(cat /opt/scripts/rate_limiter.lua)"
Bu script’i test etmek için:
#!/bin/bash
# /opt/scripts/test_rate_limiter.sh
REDIS_HOST="localhost"
REDIS_PORT="6379"
TEST_KEY="test:user:42"
WINDOW=60 # 60 saniyelik pencere
LIMIT=5 # Maksimum 5 istek
echo "Rate limiter testi başlıyor..."
echo "Limit: $LIMIT istek / $WINDOW saniye"
echo "---"
for i in $(seq 1 8); do
NOW=$(date +%s%3N) # Milisaniye cinsinden timestamp
RESULT=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT EVAL "
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. math.random())
redis.call('PEXPIRE', key, window * 1000)
return {1, limit - count - 1}
else
return {0, 0}
end
" 1 "$TEST_KEY" "$WINDOW" "$LIMIT" "$NOW")
ALLOWED=$(echo $RESULT | awk '{print $1}')
REMAINING=$(echo $RESULT | awk '{print $2}')
if [ "$ALLOWED" = "1" ]; then
echo "İstek $i: KABUL EDILDI | Kalan: $REMAINING"
else
echo "İstek $i: REDDEDILDI (429) | Rate limit aşıldı"
fi
sleep 0.1
done
# Cleanup
redis-cli DEL "$TEST_KEY" > /dev/null
API Gateway ile Merkezi Yönetim
Mikroservis mimarisinde her servisin kendi rate limiting’ini yapması yerine merkezi bir API gateway kullanmak çok daha mantıklıdır. Kong Gateway bu iş için harika bir seçenektir.
# Kong Gateway kurulumu (Docker)
docker run -d --name kong-database
-e POSTGRES_USER=kong
-e POSTGRES_DB=kong
-e POSTGRES_PASSWORD=kongpass
postgres:13
docker run --rm
--link kong-database:kong-database
-e KONG_DATABASE=postgres
-e KONG_PG_HOST=kong-database
-e KONG_PG_PASSWORD=kongpass
kong:latest kong migrations bootstrap
docker run -d --name kong
--link kong-database:kong-database
-e KONG_DATABASE=postgres
-e KONG_PG_HOST=kong-database
-e KONG_PG_PASSWORD=kongpass
-e KONG_PROXY_ACCESS_LOG=/dev/stdout
-e KONG_ADMIN_ACCESS_LOG=/dev/stdout
-e KONG_PROXY_ERROR_LOG=/dev/stderr
-e KONG_ADMIN_ERROR_LOG=/dev/stderr
-p 8000:8000
-p 8443:8443
-p 8001:8001
kong:latest
# Servis oluştur
curl -X POST http://localhost:8001/services
--data name=my-api
--data url=http://backend:8080
# Route ekle
curl -X POST http://localhost:8001/services/my-api/routes
--data paths[]=/api/v1
# Rate limiting plugin'i etkinleştir
curl -X POST http://localhost:8001/services/my-api/plugins
--data name=rate-limiting
--data config.minute=100
--data config.hour=1000
--data config.day=10000
--data config.policy=redis
--data config.redis_host=redis-host
--data config.redis_port=6379
--data config.limit_by=consumer
echo "Kong rate limiting yapılandırması tamamlandı"
Response Headers ve İstemci Bilgilendirmesi
Rate limiting’in önemli bir parçası da istemcilere doğru bilgi vermektir. Standart header’lar şunlardır:
- X-RateLimit-Limit: Penceredeki toplam istek limiti
- X-RateLimit-Remaining: Kalan istek hakkı
- X-RateLimit-Reset: Limitin sıfırlanacağı Unix timestamp
- Retry-After: 429 dönüldüğünde kaç saniye beklemesi gerektiği
- X-RateLimit-Policy: Hangi politikanın uygulandığı (opsiyonel)
# Bash ile rate limit header'larını test etme
check_rate_limit() {
local API_URL="https://api.example.com/v1/data"
local API_KEY="your-api-key"
RESPONSE=$(curl -s -D -
-H "X-API-Key: $API_KEY"
-H "Accept: application/json"
"$API_URL"
-o /tmp/api_response.json)
HTTP_STATUS=$(echo "$RESPONSE" | grep "^HTTP" | awk '{print $2}')
LIMIT=$(echo "$RESPONSE" | grep -i "x-ratelimit-limit:" | awk '{print $2}' | tr -d 'r')
REMAINING=$(echo "$RESPONSE" | grep -i "x-ratelimit-remaining:" | awk '{print $2}' | tr -d 'r')
RESET=$(echo "$RESPONSE" | grep -i "x-ratelimit-reset:" | awk '{print $2}' | tr -d 'r')
RETRY_AFTER=$(echo "$RESPONSE" | grep -i "retry-after:" | awk '{print $2}' | tr -d 'r')
echo "HTTP Durum: $HTTP_STATUS"
echo "Limit: $LIMIT"
echo "Kalan: $REMAINING"
if [ -n "$RESET" ]; then
RESET_DATE=$(date -d @$RESET '+%H:%M:%S' 2>/dev/null || date -r $RESET '+%H:%M:%S')
echo "Sıfırlanma: $RESET_DATE"
fi
if [ "$HTTP_STATUS" = "429" ]; then
echo "RATE LIMIT AŞILDI!"
echo "Bekleme süresi: ${RETRY_AFTER}s"
echo "$(date): Rate limit aşıldı, ${RETRY_AFTER}s bekleniyor" >> /var/log/api_client.log
sleep "${RETRY_AFTER:-60}"
fi
}
# API çağrısı yaparken rate limit farkındalığı
make_api_call_with_backoff() {
local max_retries=5
local retry_count=0
local wait_time=1
while [ $retry_count -lt $max_retries ]; do
check_rate_limit
if [ "$HTTP_STATUS" != "429" ]; then
echo "İstek başarılı"
return 0
fi
retry_count=$((retry_count + 1))
wait_time=$((wait_time * 2)) # Exponential backoff
echo "Deneme $retry_count/$max_retries, ${wait_time}s bekleniyor..."
sleep $wait_time
done
echo "Maksimum deneme sayısına ulaşıldı" >&2
return 1
}
Monitoring ve Alerting
Rate limiting mekanizmanızı kurduktan sonra onu izlemek de bir o kadar önemlidir. Prometheus ve Grafana kombinasyonu bu iş için standarttır.
# Nginx rate limit metriklerini Prometheus formatında çıkarmak için
# /opt/scripts/nginx_rate_limit_metrics.sh
#!/bin/bash
METRICS_FILE="/tmp/nginx_rate_limit_metrics"
LOG_FILE="/var/log/nginx/access.log"
# Son dakikadaki 429 yanıtlarını say
RATE_LIMITED=$(awk -v d="$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')"
'$0 ~ d && $9 == "429"' "$LOG_FILE" | wc -l)
# Toplam istekleri say
TOTAL_REQUESTS=$(awk -v d="$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')"
'$0 ~ d' "$LOG_FILE" | wc -l)
# En çok rate limit yiyen IP'ler
TOP_OFFENDERS=$(awk -v d="$(date -d '1 minute ago' '+%d/%b/%Y:%H:%M')"
'$0 ~ d && $9 == "429" {print $1}' "$LOG_FILE" |
sort | uniq -c | sort -rn | head -5)
# Prometheus metrikleri oluştur
cat > "$METRICS_FILE" << EOF
# HELP nginx_rate_limited_requests_total Rate limited requests in last minute
# TYPE nginx_rate_limited_requests_total gauge
nginx_rate_limited_requests_total $RATE_LIMITED
# HELP nginx_total_requests_last_minute Total requests in last minute
# TYPE nginx_total_requests_last_minute gauge
nginx_total_requests_last_minute $TOTAL_REQUESTS
EOF
cat "$METRICS_FILE"
# Alert: Eğer rate limited istekler toplam isteklerin %10'unu geçiyorsa
if [ "$TOTAL_REQUESTS" -gt 0 ]; then
RATE=$(echo "scale=2; $RATE_LIMITED * 100 / $TOTAL_REQUESTS" | bc)
RATE_INT=$(echo "$RATE" | cut -d. -f1)
if [ "${RATE_INT:-0}" -gt 10 ]; then
echo "UYARI: Rate limited istekler %$RATE seviyesinde!" |
mail -s "Rate Limit Alert" [email protected]
fi
fi
Tier Bazlı Kota Yönetimi
Gerçek dünya uygulamalarında farklı kullanıcı grupları farklı limitlere sahip olur. Bu yapıyı Redis ile yönetmek oldukça etkilidir.
# Kullanıcı tier'larını Redis'e yükle
redis-cli HSET "user:tier:free" rpm 60 rph 1000 rpd 10000
redis-cli HSET "user:tier:basic" rpm 200 rph 5000 rpd 50000
redis-cli HSET "user:tier:premium" rpm 1000 rph 30000 rpd 500000
redis-cli HSET "user:tier:enterprise" rpm 5000 rph 200000 rpd 5000000
# Kullanıcıya tier ata
redis-cli SET "user:12345:tier" "premium"
redis-cli SET "user:67890:tier" "free"
# Kullanıcının mevcut tier limitlerini sorgula
get_user_limits() {
local USER_ID=$1
local TIER=$(redis-cli GET "user:${USER_ID}:tier")
if [ -z "$TIER" ]; then
TIER="free"
fi
local RPM=$(redis-cli HGET "user:tier:${TIER}" rpm)
local RPH=$(redis-cli HGET "user:tier:${TIER}" rph)
local RPD=$(redis-cli HGET "user:tier:${TIER}" rpd)
echo "Kullanıcı $USER_ID ($TIER tier):"
echo " Dakika limiti: $RPM istek"
echo " Saat limiti: $RPH istek"
echo " Gün limiti: $RPD istek"
# Mevcut kullanımı göster
local MINUTE_KEY="usage:${USER_ID}:$(date +%Y%m%d%H%M)"
local HOUR_KEY="usage:${USER_ID}:$(date +%Y%m%d%H)"
local DAY_KEY="usage:${USER_ID}:$(date +%Y%m%d)"
local USED_MIN=$(redis-cli GET "$MINUTE_KEY" 2>/dev/null || echo 0)
local USED_HOUR=$(redis-cli GET "$HOUR_KEY" 2>/dev/null || echo 0)
local USED_DAY=$(redis-cli GET "$DAY_KEY" 2>/dev/null || echo 0)
echo " Dakika kullanımı: ${USED_MIN:-0}/$RPM"
echo " Saat kullanımı: ${USED_HOUR:-0}/$RPH"
echo " Gün kullanımı: ${USED_DAY:-0}/$RPD"
}
get_user_limits 12345
Yaygın Hatalar ve Dikkat Edilmesi Gerekenler
Rate limiting yaparken sık yapılan hatalardan kaçınmak için şunlara dikkat edin:
- Shared IP problemi: Şirket NAT’ı arkasındaki yüzlerce kullanıcıyı aynı IP ile limitlemek yanlıştır. IP yerine API key veya kullanıcı ID kullanın
- Yanlış 503 döndürme: Rate limit için 503 değil, mutlaka 429 Too Many Requests kullanın
- Retry-After header’ı eksikliği: İstemci ne kadar bekleyeceğini bilmezse saldırgan döngüye girer
- Redis single point of failure: Redis Sentinel veya Cluster kullanmadan rate limiting kritik hata noktası olabilir
- Race condition: Sayaç artırma işlemlerini mutlaka atomik yapın, Lua script veya Redis transaction kullanın
- Beyaz liste unutma: Health check endpoint’leri, internal servisler ve monitoring araçlarını limite dahil etmeyin
- Asimetrik limitler: Read (GET) ve write (POST/PUT/DELETE) operasyonlarına farklı limitler uygulamayı değerlendirin
Sonuç
Rate limiting, “bir gün kurarız” diyerek ertelenen ama sonra pahalıya mal olan bir konudur. Kademeli bir yaklaşım öneririm: Önce Nginx seviyesinde basit IP tabanlı limitler koyun, ardından uygulama katmanında API key bazlı, tier’lara göre ayrıştırılmış limitler ekleyin. Son olarak monitoring ve alerting kurarak kör noktaları ortadan kaldırın.
Yanlış yapılandırılmış bir rate limiter, meşru kullanıcıları engelleyip sizi müşteri kaybettirtebilir. Doğru yapılandırılmış biri ise hem sizi kötü aktörlerden korur hem de kaynaklarınızı verimli kullandırır. Algoritma seçimi, pencere boyutu ve limit değerleri için kesin bir formül yoktur. Kendi kullanıcı profilinizi analiz edin, makul başlangıç değerleri belirleyin ve metriklerinize bakarak ince ayar yapın. Rate limiting bir kez kurup unutulan değil, yaşayan bir sistemdir.
