RRedis Handbook

İLERİ

Rate Limiting

.NET 7+ AddRateLimiter() middleware'i ile built-in rate limiting. Redis backing store ile multi-instance'da paylaşımlı limit.

Kod örneği görünümü Bu sayfadaki eşleşen örnekleri seçilen istemciye göre gösterir.

Ne Zaman Rate Limiting Kullan / Kullanma

Kullan Kullanma Gerçek Hayat
Public API (abuse koruması) Internal service-to-service (trust boundary içi) SaaS API: Free plan 100 req/dk, Pro plan 1000 req/dk
Login/register endpoint (brute-force) Batch job / admin endpoint (kendi sisteminiz) Auth: 5 başarısız login → 15dk ban (credential stuffing engeli)
Expensive endpoint (rapor, export) Zaten queue ile throttle edilen işlemler Export: CSV export max 2/saat (her biri 30s CPU)
Webhook receiver (upstream spike) Read-only cached endpoint (zaten ucuz) Payment gateway: Webhook flood'unda kendi DB'ni koru

Endpoint tipine göre algoritma seçimi:

  • Login/Register: Fixed Window (basit, 5 attempt/15dk yeterli)
  • Public REST API: Sliding Window (adil, burst yok, kullanıcı-facing)
  • Mobile push notification: Token Bucket (batch gönderim spike'larına tolerans)
  • Payment gateway callback: Leaky Bucket (downstream DB'ye sabit yük)
  • WebSocket message: Token Bucket (chat'te hızlı mesaj burst'ü normal)

Algoritma Seçimi

Algoritma Nasıl Çalışır Avantaj Dezavantaj Ne Zaman
Fixed Window Sabit zaman diliminde sayaç (ör: 0:00-1:00) Basit, O(1) memory Pencere sınırında burst (2× limit) Internal API, basit koruma
Sliding Window Sorted Set + zaman bazlı kayan pencere Burst yok, adil dağılım O(N) memory (her request bir entry) Public API, kullanıcı-facing
Token Bucket Sabit hızla token eklenir, request token tüketir Burst'a izin (bucket kapasitesi kadar) Biraz daha karmaşık Spike-tolerant API'lar
Leaky Bucket Queue + sabit çıkış hızı Tamamen düz throughput Burst tolere etmez, latency ekler Downstream koruma, payment

Önerilen: Çoğu API için sliding window yeterli. Burst istiyorsan token bucket. Downstream'i koruyorsan leaky bucket.

Token Bucket Burst'a izin verir (bucket doluyken) sabit hızla token eklenir ↓ refill rate Bucket capacity=10 Request token tüketir burst OK! bucket boş → 429 ✓ Spike tolere eder (bucket kadar burst) Leaky Bucket Sabit çıkış hızı, burst yok gelen request'ler (değişken hız) ↓ hepsi kuyruğa girer Queue FIFO buffer sabit hız ↓ (1 req/100ms) Backend düz throughput queue dolu → 429 ✓ Downstream koruması (sabit yük) Spike-tolerant API → Token Bucket Payment/downstream → Leaky Bucket
Sliding Window (limit=5, window=60s) t-60s t-40s t-20s now t+20s limit=5 0 2 4 5 429! ← pencere kayar → (eski request'ler düşer, yeni slot açılır)

Sliding Window (Production-ready)

# Sorted Set sliding window
ZREMRANGEBYSCORE rate:user:1001 0 <now-60000>
ZCARD rate:user:1001
# < limit ise:
ZADD rate:user:1001 <now> "<now>:<random>"
PEXPIRE rate:user:1001 60000
public class RateLimiterService
{
    private readonly IDatabase _redis;

    private static readonly LuaScript _slidingWindowScript = LuaScript.Prepare(@"
        local key = @key
        local limit = tonumber(@limit)
        local window = tonumber(@window)
        local now = tonumber(@now)
        redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
        local count = redis.call('ZCARD', key)
        if count < limit then
            redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
            redis.call('PEXPIRE', key, window)
            return count + 1
        end
        return -1
    ");

    public RateLimiterService(IConnectionMultiplexer mux)
        => _redis = mux.GetDatabase();

    public async Task<RateLimitResult> CheckAsync(string clientId, int limit, TimeSpan window)
    {
        var result = (int)await _redis.ScriptEvaluateAsync(_slidingWindowScript, new
        {
            key = (RedisKey)$"rate:{clientId}",
            limit,
            window = (long)window.TotalMilliseconds,
            now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
        });

        return new RateLimitResult
        {
            IsAllowed = result > 0,
            CurrentCount = result > 0 ? result : limit,
            Limit = limit
        };
    }
}

// Middleware
public class RateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RateLimiterService _rateLimiter;

    public RateLimitMiddleware(RequestDelegate next, RateLimiterService rateLimiter)
    {
        _next = next;
        _rateLimiter = rateLimiter;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var clientId = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var result = await _rateLimiter.CheckAsync(clientId, limit: 100, TimeSpan.FromMinutes(1));

        context.Response.Headers["X-RateLimit-Limit"] = "100";
        context.Response.Headers["X-RateLimit-Remaining"] =
            Math.Max(0, 100 - result.CurrentCount).ToString();

        if (!result.IsAllowed)
        {
            context.Response.StatusCode = 429;
            await context.Response.WriteAsync("Rate limit exceeded");
            return;
        }

        await _next(context);
    }
}

ASP.NET Built-in Rate Limiter + Redis

// NuGet: dotnet add package System.Threading.RateLimiting

builder.Services.AddRateLimiter(options =>
{
    // Global sliding window — Redis-backed (custom partitioner)
    options.AddPolicy("api", httpContext =>
    {
        var clientIp = httpContext.Connection.RemoteIpAddress?.ToString() ?? "anon";
        return RateLimitPartition.GetSlidingWindowLimiter(clientIp, _ =>
            new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                SegmentsPerWindow = 6, // 10 saniyelik segment'ler
                QueueLimit = 0
            });
    });

    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.Headers["Retry-After"] = "60";
        await context.HttpContext.Response.WriteAsync(
            "Rate limit exceeded. Try again later.", ct);
    };
});

app.UseRateLimiter();

// Endpoint'e uygula
app.MapGet("/api/data", () => Results.Ok())
    .RequireRateLimiting("api");

Built-in vs Custom Lua: Built-in rate limiter tek instance'da memory-based çalışır — multi-instance'da her pod kendi limit'ini tutar. Gerçek distributed limit için yukarıdaki Lua-based sliding window örneğini kullan.