← Kursa Dön
📄 Text · 35 min

Rate Limiting

Rate limiting (hız sınırlama), bir API'nin belirli bir zaman diliminde kabul edeceği istek sayısını sınırlama mekanizmasıdır. Bu mekanizma, API'nizi aşırı yüklenme, kötü niyetli saldırılar (DDoS), ve kaynakların adil paylaşımı konularında korur. Bu derste rate limiting algoritmalarını, Bucket4j kütüphanesini ve Spring Boot interceptor tabanlı implementasyonu inceleyeceğiz.

Neden Rate Limiting?

  1. Abuse koruması — Botlar veya kötü niyetli kullanıcılar API'nizi saniyede binlerce istekle bombardımana tutabilir.

  2. Kaynak koruması — Veritabanı bağlantıları, CPU, bellek gibi kaynaklar sınırlıdır.

  3. Adil kullanım — Bir kullanıcının tüm kapasiteyi tüketmesini engelleyerek diğer kullanıcılara da hizmet verilmesini sağlar.

  4. Maliyet kontrolü — Cloud ortamda her istek maliyettir. Sınırsız istek kabul etmek fatura şoklarına yol açar.

  5. SLA uyumu — API'nize belirli bir servis seviyesi garantisi veriyorsanız, rate limiting bunu korumanın yoludur.

Rate Limiting Algoritmaları

1. Token Bucket

En yaygın algoritmadır. Bir "kova" (bucket) belirli sayıda "jeton" (token) tutar. Her istek bir jeton harcar. Jetonlar sabit hızda yenilenir:

Kova kapasitesi: 10 token
Yenilenme hızı: 2 token/saniye

t=0: Kova dolu [10 token]
t=0: 5 istek geldi → 5 token harcandı → [5 token kaldı]
t=1: 2 token eklendi → [7 token]
t=1: 3 istek geldi → [4 token kaldı]
t=2: 2 token eklendi → [6 token]
t=2: 8 istek geldi → 6 kabul, 2 reddedildi (429)

Avantaj: Burst trafiğe izin verir (kova doluyken kısa süreli yoğun trafik kabul edilir). Dezavantaj: Kısa süreli spike'larda limit aşılabilir.

2. Leaky Bucket

İstekler bir "sızdıran kova"ya eklenir ve sabit hızda işlenir. Kova dolduğunda yeni istekler reddedilir:

Kova kapasitesi: 10 istek
İşleme hızı: 2 istek/saniye

İstekler kovaya eklenir → kova sabit hızda boşalır
Kova dolarsa → 429 Too Many Requests

Avantaj: Çıkış hızı sabittir, downstream sistemi korur. Dezavantaj: Burst trafiğe izin vermez, meşru spike'larda gecikme yaşanır.

3. Sliding Window

Zaman penceresi kaydırılarak her an son N saniyedeki istek sayısı kontrol edilir:

Pencere: 60 saniye, Limit: 100 istek

t=30s'da kontrol: son 60s'deki (t=-30 → t=30) istek sayısı ≤ 100?
t=31s'da kontrol: son 60s'deki (t=-29 → t=31) istek sayısı ≤ 100?

Avantaj: Fixed window'daki sınır geçişi sorununu çözer. Dezavantaj: Her isteğin zaman damgasını tutmak bellek kullanır.

Bucket4j ile Rate Limiting

Bucket4j, Token Bucket algoritmasının Java implementasyonudur ve Spring Boot ile mükemmel çalışır:

<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
</dependency>

Temel kullanım:

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;

// Dakikada 20 istek, sabit yenilenme
Bucket bucket = Bucket.builder()
    .addLimit(Bandwidth.classic(
        20,                            // kapasite
        Refill.greedy(20, Duration.ofMinutes(1))  // yenilenme
    ))
    .build();

// İstek geldiğinde
if (bucket.tryConsume(1)) {
    // İsteği işle
} else {
    // 429 Too Many Requests
}

Spring Interceptor ile Rate Limiting

Her kullanıcı için ayrı bir bucket oluşturan Spring MVC interceptor'ı:

@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    private final Map<String, Bucket> buckets =
        new ConcurrentHashMap<>();

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        String clientId = resolveClientId(request);
        Bucket bucket = buckets.computeIfAbsent(
            clientId, this::createBucket);

        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);

        // Rate limit header'ları ekle
        response.addHeader("X-Rate-Limit-Remaining",
            String.valueOf(probe.getRemainingTokens()));

        if (probe.isConsumed()) {
            return true; // devam et
        }

        // Limit aşıldı
        long waitSeconds = probe.getNanosToWaitForRefill()
            / 1_000_000_000;
        response.addHeader("X-Rate-Limit-Retry-After-Seconds",
            String.valueOf(waitSeconds));
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.setContentType("application/json");
        response.getWriter().write("""
            {
              "status": 429,
              "error": "Too Many Requests",
              "message": "Rate limit exceeded. Try again in %d seconds."
            }
            """.formatted(waitSeconds));
        return false;
    }

    private Bucket createBucket(String clientId) {
        return Bucket.builder()
            .addLimit(Bandwidth.classic(
                100,
                Refill.intervally(100, Duration.ofMinutes(1))))
            .build();
    }

    private String resolveClientId(HttpServletRequest request) {
        // JWT varsa kullanıcı ID kullan
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            // token'dan kullanıcı ID çıkar
            return extractUserId(authHeader);
        }
        // Yoksa IP adresi kullan
        String forwarded = request.getHeader("X-Forwarded-For");
        return forwarded != null ? forwarded.split(",")[0]
                                 : request.getRemoteAddr();
    }

    private String extractUserId(String authHeader) {
        // JWT parsing logic
        return "user-from-token";
    }
}

Interceptor'ı kaydet:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final RateLimitInterceptor rateLimitInterceptor;

    public WebConfig(RateLimitInterceptor rateLimitInterceptor) {
        this.rateLimitInterceptor = rateLimitInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/health", "/api/info");
    }
}

Annotation Tabanlı Rate Limiting

Daha granüler kontrol için özel annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int capacity() default 100;
    int refillTokens() default 100;
    int refillSeconds() default 60;
}
@GetMapping("/search")
@RateLimit(capacity = 30, refillTokens = 30, refillSeconds = 60)
public List<Product> search(@RequestParam String query) {
    return productService.search(query);
}

@PostMapping("/orders")
@RateLimit(capacity = 10, refillTokens = 10, refillSeconds = 60)
public Order createOrder(@RequestBody OrderDto dto) {
    return orderService.create(dto);
}

Standard Response Headers

Rate limiting bilgisini istemciye iletmek için standart header'lar kullanılır:

X-RateLimit-Limit: 100         → toplam limit
X-RateLimit-Remaining: 73      → kalan istek hakkı
X-RateLimit-Reset: 1708200000  → limitin sıfırlanacağı Unix timestamp
Retry-After: 30                → kaç saniye beklemeli (429 yanıtında)

Rate limiting, production API'ler için zorunlu bir güvenlik katmanıdır. Doğru algoritma seçimi ve uygun limit değerleri, API'nizin hem güvenli hem de kullanılabilir olmasını sağlar.