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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir