← Kursa Dön
📄 Text · 18 min

API Rate Limiting: Bucket4j ve Spring Cloud Gateway

Giriş — Neden Herkese Sınırsız Servis Veremezsin?

Bir büfe restoran düşün. Müşteriler sabit bir ücret ödüyor ve istedikleri kadar yiyecek alıyor. Sorun ne? Bir müşteri tabağına tüm eti yığarsa, arkasındaki müşteriler aç kalır. Çözüm: kişi başı porsiyon limiti. Her müşteri her turda en fazla 3 porsiyon alabilir. Bu, herkesin adil bir şekilde yemek yemesini sağlar.

API rate limiting tam olarak bu. Bir API endpoint'ine belirli bir zaman diliminde yapılabilecek istek sayısını sınırlamaktır. Neden gerekli?

  • Kaynak koruması: Bir kullanıcı saniyede 10.000 istek atarsa, sunucu diğer kullanıcılara hizmet veremez

  • DDoS koruması: Kötü niyetli trafik, sunucuyu çökertebilir. Rate limiting ilk savunma hattıdır

  • Maliyet kontrolü: Cloud ortamında her istek para. Kontrolsüz trafik = şişmiş fatura

  • Adil kullanım: Free plan kullanıcıları sınırsız API çağrısı yapmamalı. Premium plan daha yüksek limit alır

  • Downstream koruma: Senin API'n dayanıklı olsa bile arkadaki veritabanı veya üçüncü parti servis olmayabilir

Twitter (X), GitHub, Google Maps, Stripe — büyük API'lerin hepsi rate limiting uygular. Bu bir seçenek değil, production API'nin olmazsa olmazıdır.


Rate Limiting Algoritmaları

Rate limiting'in birden fazla algoritması var. Her birinin farklı davranışı, avantajı ve kullanım alanı var. Doğru algoritmayı seçmek, uygulamanın ihtiyacına bağlı.

Fixed Window (Sabit Pencere)

En basit algoritma. Zamanı sabit pencerelere böler (örneğin her 1 dakika) ve her pencerede izin verilen istek sayısını sayar.

Limit: 100 istek / dakika

Pencere 1 (00:00 - 01:00):  ████████████ 87 istek  → OK
Pencere 2 (01:00 - 02:00):  ██████████████ 100 istek → OK (tam limitte)
Pencere 3 (02:00 - 03:00):  █████████████████ 120 istek → 20 istek reddedildi!

Sorun: Pencere sınırında burst olabilir. 00:59'da 100 istek + 01:00'da 100 istek = 2 saniyede 200 istek. Fixed window buna izin verir çünkü iki ayrı pencerededir. Bu "boundary burst" problemi güvenlik açısından sorunlu olabilir.

Sliding Window (Kayan Pencere)

Fixed window'un boundary burst sorununu çözer. Sabit pencereler yerine, her isteğin zamanını kaydeder ve son N dakikadaki istek sayısını kontrol eder.

Limit: 100 istek / dakika (sliding)

Zaman: 00:45   Son 1 dk'da: 80 istek   → OK (80 < 100)
Zaman: 00:50   Son 1 dk'da: 95 istek   → OK (95 < 100)
Zaman: 00:55   Son 1 dk'da: 100 istek  → LIMIT! (100 = 100)
Zaman: 01:05   Son 1 dk'da: 70 istek   → OK (eski istekler pencereden çıktı)

Daha adil ama daha fazla bellek gerektirir çünkü her isteğin timestamp'ini saklamalısın. Pratikte sliding window log ve sliding window counter olmak üzere iki varyantı var — counter varyantı bellek açısından daha verimlidir.

Token Bucket (Jeton Kovası)

En yaygın algoritma. Bir kova düşün: belirli hızda jeton dolar, her istek bir jeton harcar. Kova boşalırsa istek reddedilir.

Kapasite: 10 jeton | Dolum hızı: 1 jeton/saniye

t=0:   Kova: [██████████] 10 jeton
t=0:   5 istek geldi → 5 jeton harcandı
       Kova: [█████     ] 5 jeton
t=3:   3 jeton doldu
       Kova: [████████  ] 8 jeton
t=3:   10 istek geldi → 8 kabul, 2 reddedildi
       Kova: [          ] 0 jeton
t=10:  10 jeton doldu (kapasite max 10)
       Kova: [██████████] 10 jeton

Avantaj: Burst'e izin verir ama kontrollü. Kova doluyken kısa süreli burst yapabilirsin, ama sürekli yüksek hızda istek atamazsın. Bu, gerçek dünya trafiğine en uygun davranıştır.

Leaky Bucket (Sızdıran Kova)

Token bucket'ın tersi gibi düşün. İstekler kovaya dolar, kova sabit hızda boşalır. Kova taşarsa istek reddedilir.

Kapasite: 10 | İşleme hızı: 2 istek/saniye

t=0:   8 istek geldi → kovaya eklendi [████████  ]
t=0.5: 1 istek işlendi (sızdı)        [███████   ] + 3 yeni istek → [██████████] dolu
t=1:   2 istek işlendi                 [████████  ] + 3 yeni istek → kova taştı! 1 reddedildi

Fark: Token bucket burst'e izin verir, leaky bucket sabit hızda işler. Leaky bucket, downstream servislere sabit hızda istek göndermek istediğinde idealdir — örneğin harici bir API'nin rate limit'ini aşmamak için.

Algoritma Karşılaştırması

AlgoritmaBurst DesteğiBellekKarmaşıklıkKullanım Alanı
Fixed WindowHayır (boundary burst var)DüşükBasitBasit API limitleri
Sliding WindowHayırOrta-YüksekOrtaAdil rate limiting
Token BucketEvet (kontrollü)DüşükOrtaGenel API rate limiting
Leaky BucketHayır (sabit hız)DüşükOrtaSabit hız gerektiren durumlar

Pratikte Token Bucket en yaygın tercih. AWS API Gateway, Stripe, GitHub API — hepsi token bucket veya varyantlarını kullanır. Bucket4j kütüphanesi de token bucket implementasyonudur.


Bucket4j ile Spring MVC Rate Limiting

Bucket4j, Java için en popüler rate limiting kütüphanesidir. Token bucket algoritmasını implement eder. Spring Boot ile entegrasyonu kolaydır.

Temel Kurulum

<!-- pom.xml -->
<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
</dependency>
// Basit Bucket4j kullanımı — temel kavramları anlamak için
public class RateLimitDemo {

    public static void main(String[] args) {
        // Konfigürasyon: 10 jeton kapasiteli, saniyede 5 jeton dolan kova
        Bandwidth limit = Bandwidth.builder()
            .capacity(10)                           // Kova kapasitesi
            .refillGreedy(5, Duration.ofSeconds(1)) // Saniyede 5 jeton dol
            .build();

        Bucket bucket = Bucket.builder()
            .addLimit(limit)
            .build();

        // İstek simülasyonu
        for (int i = 1; i <= 15; i++) {
            if (bucket.tryConsume(1)) {  // 1 jeton harca
                System.out.println("İstek " + i + ": ✅ Kabul edildi");
            } else {
                System.out.println("İstek " + i + ": ❌ Rate limit aşıldı!");
            }
        }
        // İlk 10 istek kabul edilir (kova dolu), sonraki 5 reddedilir
    }
}

refillGreedy ve refillIntervally arasındaki fark önemli. refillGreedy jetonları mümkün olan en kısa sürede doldurur (saniyede 5 → her 200ms'de 1). refillIntervally ise tüm jetonları periyodun sonunda toplu olarak ekler (her saniyenin sonunda 5 birden). API rate limiting için genellikle refillGreedy tercih edilir — daha düzgün bir trafik akışı sağlar.

Spring MVC Interceptor ile Rate Limiting

Her endpoint'e ayrı ayrı rate limiting uygulamak yerine, bir interceptor ile tüm istekleri merkezi olarak kontrol edelim:

@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    // IP adresi başına ayrı bucket — her kullanıcının kendi limiti
    private final Map<String, Bucket> bucketCache = new ConcurrentHashMap<>();

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

        String clientIp = getClientIp(request);
        Bucket bucket = bucketCache.computeIfAbsent(clientIp, this::createBucket);

        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);

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

        if (probe.isConsumed()) {
            return true;  // İstek kabul edildi, devam et
        }

        // Rate limit aşıldı
        long waitSeconds = probe.getNanosToWaitForRefill() / 1_000_000_000;
        response.setHeader("Retry-After", String.valueOf(waitSeconds));
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.setContentType("application/json");
        response.getWriter().write("""
            {
                "error": "Too Many Requests",
                "message": "Rate limit aşıldı. %d saniye sonra tekrar deneyin.",
                "retryAfter": %d
            }
            """.formatted(waitSeconds, waitSeconds));
        return false;  // İsteği reddet
    }

    private Bucket createBucket(String key) {
        // Her IP için: 100 istek/dakika, burst 20
        Bandwidth limit = Bandwidth.builder()
            .capacity(100)
            .refillGreedy(100, Duration.ofMinutes(1))
            .build();

        Bandwidth burstLimit = Bandwidth.builder()
            .capacity(20)
            .refillGreedy(20, Duration.ofSeconds(10))
            .build();

        return Bucket.builder()
            .addLimit(limit)       // Dakikada 100 istek
            .addLimit(burstLimit)  // 10 saniyede 20 istek (burst kontrolü)
            .build();
    }

    private String getClientIp(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}
// Interceptor'ı WebMvcConfigurer ile 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/**")      // Sadece API endpoint'lerine uygula
            .excludePathPatterns(
                "/api/health",               // Health check'i hariç tut
                "/api/public/**"             // Public endpoint'leri hariç tut
            );
    }
}

Bu interceptor iki katmanlı rate limiting uygular: dakikada 100 istek (genel limit) ve 10 saniyede 20 istek (burst kontrolü). İki limit birlikte çalışır — her ikisinin de izin verdiği istekler kabul edilir.

⚠️ `ConcurrentHashMap` ile in-memory bucket cache, tek instance için çalışır. Birden fazla instance (load balancer arkasında) varsa, bir kullanıcı her instance'a ayrı ayrı 100 istek atabilir — toplam 400 istek. Distributed rate limiting için Redis gerekir — bunu ileride göreceğiz.

Annotation-Based Rate Limiting

Daha esnek bir yaklaşım olarak, endpoint bazında farklı limitler tanımlamak için custom annotation kullanabiliriz:

// Custom annotation tanımı
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int capacity() default 100;      // Kova kapasitesi
    int refillTokens() default 100;  // Dolum miktarı
    int refillSeconds() default 60;  // Dolum periyodu (saniye)
}
// Annotation'ı işleyen Aspect
@Aspect
@Component
public class RateLimitAspect {

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

    @Around("@annotation(rateLimit)")
    public Object enforce(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes)
            RequestContextHolder.currentRequestAttributes()).getRequest();

        String key = getClientIp(request) + ":" + joinPoint.getSignature().toShortString();

        Bucket bucket = bucketCache.computeIfAbsent(key, k ->
            Bucket.builder()
                .addLimit(Bandwidth.builder()
                    .capacity(rateLimit.capacity())
                    .refillGreedy(rateLimit.refillTokens(),
                        Duration.ofSeconds(rateLimit.refillSeconds()))
                    .build())
                .build()
        );

        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);

        if (!probe.isConsumed()) {
            long waitSeconds = probe.getNanosToWaitForRefill() / 1_000_000_000;
            throw new RateLimitExceededException(waitSeconds);
        }

        // Header'ları ekle
        HttpServletResponse response = ((ServletRequestAttributes)
            RequestContextHolder.currentRequestAttributes()).getResponse();
        response.setHeader("X-RateLimit-Remaining",
            String.valueOf(probe.getRemainingTokens()));

        return joinPoint.proceed();  // Asıl metodu çalıştır
    }

    private String getClientIp(HttpServletRequest request) {
        String xff = request.getHeader("X-Forwarded-For");
        return (xff != null) ? xff.split(",")[0].trim() : request.getRemoteAddr();
    }
}
// Controller'da kullanım — endpoint bazında farklı limitler
@RestController
@RequestMapping("/api")
public class ProductController {

    // Listeleme: dakikada 200 istek
    @RateLimit(capacity = 200, refillTokens = 200, refillSeconds = 60)
    @GetMapping("/products")
    public List<Product> listProducts() {
        return productService.findAll();
    }

    // Arama: dakikada 50 istek (veritabanı yoğun)
    @RateLimit(capacity = 50, refillTokens = 50, refillSeconds = 60)
    @GetMapping("/products/search")
    public List<Product> searchProducts(@RequestParam String query) {
        return productService.search(query);
    }

    // Sipariş oluşturma: dakikada 10 istek (kritik işlem)
    @RateLimit(capacity = 10, refillTokens = 10, refillSeconds = 60)
    @PostMapping("/orders")
    public Order createOrder(@RequestBody OrderRequest request) {
        return orderService.create(request);
    }
}

Bu yaklaşımla her endpoint'e farklı limit verebilirsin. Listeleme gibi hafif endpoint'lere yüksek limit, sipariş oluşturma gibi ağır endpoint'lere düşük limit — kaynak kullanımına göre optimize edilmiş rate limiting.


Spring Cloud Gateway Rate Limiting

Microservice mimarisinde rate limiting genellikle API Gateway seviyesinde yapılır. Her microservice'e ayrı ayrı rate limiting eklemek yerine, tek noktada kontrol edersin.

Gateway Kurulumu

<!-- pom.xml — Gateway dependencies -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
# application.yml — Gateway rate limiting konfigürasyonu
spring:
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: http://product-service:8081
          predicates:
            - Path=/api/products/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10   # Saniyede 10 istek
                redis-rate-limiter.burstCapacity: 20    # Burst: 20 istek
                redis-rate-limiter.requestedTokens: 1   # İstek başına 1 jeton
                key-resolver: "#{@ipKeyResolver}"       # IP bazlı limit

        - id: order-service
          uri: http://order-service:8082
          predicates:
            - Path=/api/orders/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 5
                redis-rate-limiter.burstCapacity: 10
                key-resolver: "#{@userKeyResolver}"     # Kullanıcı bazlı limit

  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
// Key Resolver — kimin limitini sayacağını belirler
@Configuration
public class RateLimiterConfig {

    // IP adresi bazlı — anonim kullanıcılar için
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            Optional.ofNullable(exchange.getRequest().getRemoteAddress())
                .map(addr -> addr.getAddress().getHostAddress())
                .orElse("unknown")
        );
    }

    // Kullanıcı ID bazlı — authenticated kullanıcılar için
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("X-User-Id"))
                .orElse(
                    Optional.ofNullable(exchange.getRequest().getRemoteAddress())
                        .map(addr -> addr.getAddress().getHostAddress())
                        .orElse("anonymous")
                )
        );
    }

    // API key bazlı — third-party entegrasyonlar için
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(
            Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("X-API-Key"))
                .orElse("no-api-key")
        );
    }
}

Spring Cloud Gateway, Redis üzerinde token bucket algoritmasını kullanır. replenishRate saniyede kaç jeton dolacağını, burstCapacity kova kapasitesini belirler. Tüm instance'lar aynı Redis'i kullandığı için distributed rate limiting otomatik olarak çalışır.

key-resolver parametresi çok önemli. IP bazlı limitlemede aynı ofisteki tüm kullanıcılar aynı IP'yi paylaşır — biri çok istek atarsa diğerleri de etkilenir. Kullanıcı bazlı limitleme daha adil ama authentication gerektirir. API key bazlı limitleme ise B2B entegrasyonlarda en yaygın yaklaşımdır.


Redis-Backed Distributed Rate Limiting

Birden fazla uygulama instance'ı çalışıyorsa (ki production'da çalışıyordur), in-memory rate limiting yetersiz kalır. Redis, merkezi bir sayaç görevi görür.

Bucket4j + Redis Entegrasyonu

<!-- pom.xml — Bucket4j Redis desteği -->
<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-redis</artifactId>
    <version>8.10.1</version>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>
@Configuration
public class DistributedRateLimitConfig {

    @Bean
    public ProxyManager<String> proxyManager(RedisConnectionFactory connectionFactory) {
        // Lettuce Redis client oluştur
        RedisClient redisClient = RedisClient.create(
            RedisURI.builder()
                .withHost("localhost")
                .withPort(6379)
                .build()
        );

        StatefulRedisConnection<String, byte[]> connection =
            redisClient.connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE));

        return LettuceBasedProxyManager.builderFor(connection)
            .withExpirationStrategy(
                ExpirationAfterWriteStrategy.basedOnTimeForRefillingBucketUpToMax(
                    Duration.ofMinutes(5)  // Kullanılmayan bucket'lar 5 dk sonra silinir
                )
            )
            .build();
    }
}
@Service
public class DistributedRateLimitService {

    private final ProxyManager<String> proxyManager;

    // Plan bazlı bucket konfigürasyonları
    private static final Map<String, BucketConfiguration> PLAN_CONFIGS = Map.of(
        "FREE", createConfig(20, 60),       // Free: 20 istek/dakika
        "BASIC", createConfig(100, 60),     // Basic: 100 istek/dakika
        "PREMIUM", createConfig(1000, 60),  // Premium: 1000 istek/dakika
        "ENTERPRISE", createConfig(10000, 60) // Enterprise: 10000 istek/dakika
    );

    public DistributedRateLimitService(ProxyManager<String> proxyManager) {
        this.proxyManager = proxyManager;
    }

    public RateLimitResult tryConsume(String userId, String plan) {
        String key = "rate-limit:" + userId;

        BucketConfiguration config = PLAN_CONFIGS.getOrDefault(plan,
            PLAN_CONFIGS.get("FREE"));

        // Redis'ten bucket al veya oluştur
        Bucket bucket = proxyManager.builder()
            .build(key, () -> config);

        ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);

        return new RateLimitResult(
            probe.isConsumed(),
            probe.getRemainingTokens(),
            probe.getNanosToWaitForRefill() / 1_000_000_000
        );
    }

    private static BucketConfiguration createConfig(int capacity, int refillSeconds) {
        return BucketConfiguration.builder()
            .addLimit(Bandwidth.builder()
                .capacity(capacity)
                .refillGreedy(capacity, Duration.ofSeconds(refillSeconds))
                .build())
            .build();
    }

    public record RateLimitResult(
        boolean allowed,
        long remainingTokens,
        long retryAfterSeconds
    ) {}
}

Bu implementasyon plan bazlı rate limiting sağlar. Free kullanıcılar dakikada 20, Premium kullanıcılar 1000 istek atabilir. Tüm bucket durumları Redis'te saklanır — 5 instance'ın arkasındaki kullanıcı tek bir limit paylaşır.

💡 Redis key'lerinde TTL (expiration) ayarlamak kritik. Aktif olmayan kullanıcıların bucket'ları Redis'te sonsuza kadar kalırsa bellek şişer. ExpirationAfterWriteStrategy ile kullanılmayan bucket'lar otomatik temizlenir. Büyük ölçekli sistemlerde milyonlarca kullanıcı olabilir — her birinin bucket'ı 50-100 byte bile tutsa toplam gigabyte'larca bellek gerekir.


HTTP Header'lar ve Hata Yanıtları

Rate limiting'in kullanıcı deneyimi açısından en önemli kısmı, doğru HTTP header ve hata yanıtları döndürmektir. İstemci (frontend, mobil uygulama, üçüncü parti) bu header'lara bakarak davranışını ayarlar.

Standart Rate Limit Header'ları

HTTP/1.1 200 OK
X-RateLimit-Limit: 100              ← Toplam izin verilen istek
X-RateLimit-Remaining: 73           ← Kalan istek hakkı
X-RateLimit-Reset: 1708963200       ← Limitin sıfırlanacağı Unix timestamp

---

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708963200
Retry-After: 45                      ← Kaç saniye sonra tekrar dene
Content-Type: application/json

{
    "error": "Too Many Requests",
    "message": "Rate limit aşıldı. 45 saniye sonra tekrar deneyin.",
    "limit": 100,
    "remaining": 0,
    "retryAfter": 45
}

Spring Boot'ta Global Rate Limit Response

// Global exception handler — rate limit aşıldığında
@RestControllerAdvice
public class RateLimitExceptionHandler {

    @ExceptionHandler(RateLimitExceededException.class)
    public ResponseEntity<Map<String, Object>> handleRateLimit(
            RateLimitExceededException ex) {

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", Instant.now());
        body.put("status", 429);
        body.put("error", "Too Many Requests");
        body.put("message", "API rate limit aşıldı. " +
            ex.getRetryAfterSeconds() + " saniye sonra tekrar deneyin.");
        body.put("retryAfter", ex.getRetryAfterSeconds());

        return ResponseEntity
            .status(HttpStatus.TOO_MANY_REQUESTS)
            .header("Retry-After", String.valueOf(ex.getRetryAfterSeconds()))
            .header("X-RateLimit-Remaining", "0")
            .body(body);
    }
}
// Custom exception
public class RateLimitExceededException extends RuntimeException {

    private final long retryAfterSeconds;

    public RateLimitExceededException(long retryAfterSeconds) {
        super("Rate limit exceeded. Retry after " + retryAfterSeconds + " seconds.");
        this.retryAfterSeconds = retryAfterSeconds;
    }

    public long getRetryAfterSeconds() {
        return retryAfterSeconds;
    }
}

İstemci Tarafında Retry Mantığı

İyi bir API client'ı Retry-After header'ına bakarak otomatik retry yapar. 429 yanıtı aldığında header'daki süre kadar bekler, header yoksa exponential backoff uygular (1s, 2s, 4s, 8s...). AWS SDK, Google Client, Stripe SDK gibi tüm büyük SDK'larda bu pattern standarttır.


Gerçek Dünya — Çok Katmanlı Rate Limiting

Production'da rate limiting tek katmanlı değildir. Dört seviyede farklı limitler uygulanır:

  1. CDN/WAF katmanı — Global DDoS koruması (10K req/s, IP ban)

  2. API Gateway katmanı — API key bazlı iş mantığı limiti (1K req/min)

  3. Application katmanı — Kullanıcı/endpoint bazlı limit (plan'a göre değişir)

  4. Database katmanı — Connection pool ve query timeout (kaynak koruması)

Her katman farklı bir tehdidi engeller. Pratikte OncePerRequestFilter chain'i ile implement edersin — herhangi bir katman 429 döndürürse istek zincirin geri kalanına ulaşmaz.

⚠️ Rate limiting bypass'ına dikkat. X-Forwarded-For header'ı istemci tarafından manipüle edilebilir. Trusted proxy listesi tanımla ve sadece güvenilir proxy'lerden gelen değerleri kabul et.


Özet

  • Rate limiting, API'yi aşırı kullanım, DDoS saldırıları ve adaletsiz kaynak tüketiminden koruyan zorunlu bir mekanizmadır — production'daki her API'de olmalıdır

  • Token Bucket en yaygın algoritmadır — kontrollü burst'e izin verir, bellek verimlidir ve gerçek dünya trafiğine en uygun davranışı sunar. Bucket4j bu algoritmayı Java'da implement eder

  • Spring MVC'de rate limiting interceptor veya custom annotation ile uygulanır — endpoint bazında farklı limitler tanımlanabilir, IP veya kullanıcı bazlı sayaçlar tutulur

  • Spring Cloud Gateway rate limiting'i API Gateway seviyesinde merkezi olarak yapar — Redis-backed token bucket ile tüm instance'lar arasında paylaşımlı limit sağlar

  • Distributed rate limiting için Redis şarttır — birden fazla instance çalışırken in-memory sayaçlar yetersiz kalır, Redis merkezi sayaç görevi görerek tüm instance'lar arasında tutarlı limit uygular

  • HTTP 429, Retry-After ve X-RateLimit header'ları standart yanıt formatıdır — istemci bu header'lara bakarak otomatik retry ve backoff uygular, iyi bir API deneyimi için bu header'ların her zaman dönmesi gerekir