← Tüm yazılara dön

API Rate Limiting — .NET 8'de Yerleşik Çözüm

API Rate Limiting — .NET 8'de Yerleşik Çözüm

API Rate Limiting — .NET 8'de Yerleşik Çözüm

Neden Rate Limiting?

API'niz internet üzerindeyse şu senaryolar kaçınılmazdır:

Senaryo 1 — Kötü niyetli kullanım:
  Bir bot saniyede 10.000 istek gönderir
  → Sunucu kaynakları tükenir
  → Gerçek kullanıcılar yanıt alamaz

Senaryo 2 — Hatalı istemci kodu:
  Bir müşterinin uygulaması sonsuz döngüde API çağrısı yapar
  → Kota aşılır, fatura patlar
  → Diğer müşteriler etkilenir

Senaryo 3 — Credential stuffing:
  Binlerce farklı şifre kombinasyonu denenir
  → Brute force ile hesap ele geçirilir

Senaryo 4 — DDoS:
  Dağıtık saldırı ile API'niz çöker
  → İş sürekliliği sekteye uğrar

Rate limiting bu senaryoların hepsine karşı ilk savunma hattıdır. .NET 7'ye kadar bu için AspNetCoreRateLimit gibi harici paketlere ihtiyaç vardı. .NET 8 ile birlikte System.Threading.RateLimiting namespace'i ve Microsoft.AspNetCore.RateLimiting middleware yerleşik olarak geldi.


Temel Kavramlar

Rate Limiting Nerede Durmalı?

İstemci
   ↓
[CDN / WAF]           ← İdeal: Burada, hiç sunucuya ulaşmadan engellenir
   ↓
[Reverse Proxy]       ← İyi: Nginx, IIS üzerinde
   ↓
[API Gateway]         ← İyi: APIM, Kong
   ↓
[.NET Middleware]     ← Bu yazının konusu
   ↓
[Controller]
   ↓
[Veritabanı]

.NET middleware seviyesindeki rate limiting CDN'in yerini tutmaz. CDN koruması olmayan API'lerde veya internal API'lerde .NET middleware son derece değerlidir.

Temel Metrikler

Limit      → Pencerede kaç isteğe izin veriliyor?
Window     → Pencere ne kadar süre?
Partition  → Limit kim için geçerli? (IP, kullanıcı, API key)
Queue      → Limit aşılınca istek beklemeye alınsın mı?

Algoritma Seçimi

1. Fixed Window — Sabit Pencere

En basit algoritma. Belirli bir zaman penceresinde sabit istek limiti.

Pencere: 1 dakika, Limit: 10

00:00 ──────────────── 01:00 ──────────────── 02:00
  │                      │                      │
  ├── 10 istek OK        ├── 10 istek OK        │
  └── 11. istek → 429    └── 11. istek → 429

Sorun — Window boundary saldırısı:

00:59 → 10 istek gönder (son saniyede)
01:00 → 10 istek daha gönder (yeni pencere)
Sonuç: 2 saniyede 20 istek — limit aşıldı ama engellenemedi
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", policy =>
    {
        policy.PermitLimit = 10;
        policy.Window = TimeSpan.FromMinutes(1);
        policy.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        policy.QueueLimit = 0; // Kuyruğa alma
    });
});

Ne zaman kullanılır:

✅ Basit API koruması
✅ Webhook endpoint'leri
✅ Düşük trafikli API'ler
❌ Window boundary saldırısı riski kabul edilemiyorsa

2. Sliding Window — Kayan Pencere

Fixed window'un boundary sorununu çözer. Pencere gerçek zamanlı olarak kayar.

Pencere: 1 dakika, Limit: 10

Şu an: 00:45
Son 1 dakika: 00:45 → 23:45 arası istekler sayılır
Bu aralıkta 7 istek yapılmış → 3 istek daha yapılabilir

Şu an: 00:46
Son 1 dakika: 00:46 → 23:46 arası istekler sayılır
Pencere kaydı, en eski istek düştü → 4 istek daha yapılabilir
builder.Services.AddRateLimiter(options =>
{
    options.AddSlidingWindowLimiter("sliding", policy =>
    {
        policy.PermitLimit = 100;
        policy.Window = TimeSpan.FromMinutes(1);
        policy.SegmentsPerWindow = 6; // Dakikayı 6 segmente böl (10'ar saniye)
        policy.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        policy.QueueLimit = 10;
    });
});

Segment mantığı:

Window: 60 saniye, SegmentsPerWindow: 6
→ Her segment 10 saniye
→ Daha hassas tracking, daha az bellek kullanımı
   (tam kayan pencere yerine)

Ne zaman kullanılır:

✅ Genel API rate limiting
✅ Kullanıcı bazlı limitler
✅ Fixed window yetersizse
❌ Yüksek bellek hassasiyeti olan ortamlar

3. Token Bucket — Token Kovası

Bir kova token'la dolar. Her istek bir token harcar. Kova belirli hızda dolar.

Kova kapasitesi: 10 token
Dolma hızı: saniyede 2 token

Başlangıç: 10 token (dolu)
10 istek gönder → 0 token
5 saniye bekle → 10 token yeniden doldu (max)
2 istek gönder → 8 token

Avantajı: Ani burst trafiğine izin verir, ama sürdürülebilir hız sınırlıdır.

builder.Services.AddRateLimiter(options =>
{
    options.AddTokenBucketLimiter("token-bucket", policy =>
    {
        policy.TokenLimit = 100;              // Kova kapasitesi
        policy.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        policy.TokensPerPeriod = 20;          // Her 10 saniyede 20 token ekle
        policy.AutoReplenishment = true;      // Otomatik doldurma
        policy.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        policy.QueueLimit = 5;
    });
});

Ne zaman kullanılır:

✅ Uygulama başladığında burst trafiğine izin vermek
✅ Ortalama hız sınırı gerekli ama anlık burst OK
✅ Mobil uygulamalar — bağlantı kurulunca birden fazla istek gelir
❌ Kesin "X istek / dakika" garantisi gerekiyorsa

4. Concurrency Limiter — Eş Zamanlı İstek

Kaç isteğin aynı anda işlenebileceğini sınırlar. Zaman penceresine değil, anlık eş zamanlılığa bakar.

Limit: 5 eş zamanlı istek

İstek 1 → İşleniyor...
İstek 2 → İşleniyor...
İstek 3 → İşleniyor...
İstek 4 → İşleniyor...
İstek 5 → İşleniyor...
İstek 6 → 429 (veya kuyrukta bekle)

İstek 1 tamamlandı → İstek 6 işlenmeye başlar
builder.Services.AddRateLimiter(options =>
{
    options.AddConcurrencyLimiter("concurrency", policy =>
    {
        policy.PermitLimit = 10;
        policy.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        policy.QueueLimit = 5;
    });
});

Ne zaman kullanılır:

✅ Yoğun kaynak tüketen endpoint'ler (rapor oluşturma, dosya işleme)
✅ Veritabanı bağlantı havuzunu korumak
✅ External API çağrısı yapan endpoint'ler
❌ Hız (rate) değil, eş zamanlılık sorunu yoksa

Partitioned Rate Limiting — Kullanıcı Bazlı Limit

Aynı limiti herkese uygulamak çoğu zaman yanlıştır. Premium kullanıcılar daha fazla, anonim kullanıcılar daha az istek yapabilmeli.

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        // Kimlik doğrulanmış kullanıcı
        if (context.User.Identity?.IsAuthenticated == true)
        {
            var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            var isPremium = context.User.IsInRole("Premium");

            return RateLimitPartition.GetSlidingWindowLimiter(
                partitionKey: $"user:{userId}",
                factory: _ => new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = isPremium ? 1000 : 100, // Premium 10x daha fazla
                    Window = TimeSpan.FromMinutes(1),
                    SegmentsPerWindow = 6,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 0
                });
        }

        // API key kullanan istemci
        if (context.Request.Headers.TryGetValue("X-Api-Key", out var apiKey))
        {
            return RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: $"apikey:{apiKey}",
                factory: _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 500,
                    Window = TimeSpan.FromMinutes(1)
                });
        }

        // Anonim — IP bazlı, en kısıtlı
        var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: $"anon:{ip}",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 10,
                Window = TimeSpan.FromMinutes(1)
            });
    });
});

Named Policy — Endpoint Bazlı Limit

Her endpoint farklı bir rate limiting politikası gerektirebilir:

builder.Services.AddRateLimiter(options =>
{
    // Login endpoint — brute force koruması
    options.AddFixedWindowLimiter("auth-strict", policy =>
    {
        policy.PermitLimit = 5;
        policy.Window = TimeSpan.FromMinutes(15);
        policy.QueueLimit = 0;
    });

    // Genel API — kullanıcı dostu
    options.AddSlidingWindowLimiter("api-standard", policy =>
    {
        policy.PermitLimit = 60;
        policy.Window = TimeSpan.FromMinutes(1);
        policy.SegmentsPerWindow = 6;
        policy.QueueLimit = 5;
    });

    // Rapor endpoint — yoğun kaynak
    options.AddConcurrencyLimiter("report-limiter", policy =>
    {
        policy.PermitLimit = 3;
        policy.QueueLimit = 2;
    });

    // Webhook — esnek burst
    options.AddTokenBucketLimiter("webhook", policy =>
    {
        policy.TokenLimit = 50;
        policy.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        policy.TokensPerPeriod = 10;
        policy.AutoReplenishment = true;
    });

    // 429 yanıtı
    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)retryAfter.TotalSeconds).ToString();
        }

        await context.HttpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 429,
            Title = "Çok Fazla İstek",
            Detail = "İstek limitinizi aştınız. Lütfen daha sonra tekrar deneyin.",
            Extensions = { ["retryAfter"] = context.Lease.TryGetMetadata(
                MetadataName.RetryAfter, out var ra) ? (int)ra.TotalSeconds : 60 }
        }, ct);
    };
});
// Controller'da politika uygulama
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    [HttpPost("login")]
    [EnableRateLimiting("auth-strict")]       // 5 deneme / 15 dakika
    public async Task<IActionResult> Login(LoginRequest request) { ... }

    [HttpPost("forgot-password")]
    [EnableRateLimiting("auth-strict")]       // Aynı politika
    public async Task<IActionResult> ForgotPassword(ForgotPasswordRequest request) { ... }
}

[ApiController]
[Route("api/products")]
[EnableRateLimiting("api-standard")]          // Controller seviyesinde — tüm action'lara
public class ProductsController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll() { ... }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(Guid id) { ... }

    [HttpGet("export")]
    [EnableRateLimiting("report-limiter")]    // Action override — daha sıkı
    public async Task<IActionResult> Export() { ... }

    [HttpGet("health")]
    [DisableRateLimiting]                     // Rate limiting yok — health check
    public IActionResult Health() => Ok();
}

Chained Rate Limiter — Zincirleme

Birden fazla limiti birleştirin — her ikisi de sağlanmalı:

// Hem saniyede 5, hem dakikada 100 istek limiti
var chainedLimiter = PartitionedRateLimiter.CreateChained(
    PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        var userId = GetUserId(context);
        return RateLimitPartition.GetFixedWindowLimiter(
            $"perSecond:{userId}",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 5,
                Window = TimeSpan.FromSeconds(1)
            });
    }),
    PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        var userId = GetUserId(context);
        return RateLimitPartition.GetFixedWindowLimiter(
            $"perMinute:{userId}",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1)
            });
    })
);

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = chainedLimiter;
});

Response Headers — İstemciye Bilgi Ver

İstemciler ne kadar limit kaldığını bilmeli. Bu sayede akıllı retry stratejisi uygulayabilirler.

// RateLimitHeadersMiddleware.cs
public class RateLimitHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public RateLimitHeadersMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        await _next(context);

        var rateLimitFeature = context.Features.Get<IRateLimiterStatisticsFeature>();

        if (rateLimitFeature is not null)
        {
            context.Response.Headers["X-RateLimit-Limit"] =
                rateLimitFeature.CurrentStatistics.CurrentAvailablePermits.ToString();

            context.Response.Headers["X-RateLimit-Remaining"] =
                rateLimitFeature.CurrentStatistics.CurrentAvailablePermits.ToString();

            context.Response.Headers["X-RateLimit-Reset"] =
                DateTimeOffset.UtcNow
                    .Add(TimeSpan.FromMinutes(1))
                    .ToUnixTimeSeconds()
                    .ToString();
        }
    }
}

// Extension
public static class RateLimitHeadersExtensions
{
    public static IApplicationBuilder UseRateLimitHeaders(
        this IApplicationBuilder app)
        => app.UseMiddleware<RateLimitHeadersMiddleware>();
}

İstemci bu header'larla ne yapar:

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1735689600

HTTP/1.1 429 Too Many Requests
Retry-After: 47
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689647

IP Whitelist — Bazı İstemcileri Muaf Tutmak

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

        // İç ağ ve monitoring araçları limit dışı
        var whitelistedIps = new HashSet<string>
        {
            "10.0.0.1",         // İç monitoring
            "10.0.0.5",         // CI/CD runner
            "192.168.1.0",      // İç ağ
            "127.0.0.1"         // Localhost
        };

        if (whitelistedIps.Contains(ip))
        {
            // Sınırsız erişim — NoLimiter
            return RateLimitPartition.GetNoLimiter(ip);
        }

        return RateLimitPartition.GetSlidingWindowLimiter(ip,
            _ => new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 60,
                Window = TimeSpan.FromMinutes(1),
                SegmentsPerWindow = 6
            });
    });
});

Distributed Rate Limiting — Redis ile

Varsayılan .NET rate limiter tek instance için in-memory çalışır. Birden fazla uygulama instance'ında partition key'ler paylaşılmaz — her instance kendi sayacını tutar.

Çözüm: Redis backed rate limiter.

dotnet add package RedisRateLimiting
using RedisRateLimiting;
using StackExchange.Redis;

var redisConnection = ConnectionMultiplexer.Connect(
    builder.Configuration.GetConnectionString("Redis")!);

builder.Services.AddRateLimiter(options =>
{
    options.AddRedisSlidingWindowLimiter("distributed", policy =>
    {
        policy.ConnectionMultiplexerFactory = () => redisConnection;
        policy.PermitLimit = 100;
        policy.Window = TimeSpan.FromMinutes(1);
    });

    options.AddRedisTokenBucketLimiter("distributed-burst", policy =>
    {
        policy.ConnectionMultiplexerFactory = () => redisConnection;
        policy.TokenLimit = 200;
        policy.TokensPerPeriod = 50;
        policy.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
    });
});

Ne zaman Redis gereklidir:

Tek instance API         → In-memory yeterli
Load balanced API        → Redis zorunlu
Kubernetes pod'ları      → Redis zorunlu
Horizontal scaling       → Redis zorunlu

Logging ve Monitoring

builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, ct) =>
    {
        var httpContext = context.HttpContext;
        var logger = httpContext.RequestServices
            .GetRequiredService<ILogger<Program>>();

        var ip = httpContext.Connection.RemoteIpAddress?.ToString();
        var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var path = httpContext.Request.Path;
        var method = httpContext.Request.Method;

        // Metrik sayacı artır — Prometheus ile izlenebilir
        RateLimitMetrics.RejectedRequests.Add(1,
            new KeyValuePair<string, object?>("path", path),
            new KeyValuePair<string, object?>("method", method));

        logger.LogWarning(
            "Rate limit aşıldı. IP: {IP} | UserId: {UserId} | {Method} {Path}",
            ip, userId ?? "anonymous", method, path);

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        await context.HttpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 429,
            Title = "Çok Fazla İstek",
            Detail = "İstek limitinizi aştınız."
        }, ct);
    };
});

// Prometheus metrikleri
public static class RateLimitMetrics
{
    public static readonly Counter<long> RejectedRequests =
        Meter.CreateCounter<long>(
            "api_rate_limit_rejected_total",
            description: "Rate limit nedeniyle reddedilen toplam istek sayısı");

    private static readonly Meter Meter = new("MyApp.RateLimit");
}

Test Etmek

// RateLimitingTests.cs
public class RateLimitingTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public RateLimitingTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task Login_ShouldReturn429_AfterFiveAttempts()
    {
        // Arrange
        var client = _factory.CreateClient();
        var loginRequest = new { Email = "test@test.com", Password = "wrong" };

        // Act — 5 başarısız deneme
        for (int i = 0; i < 5; i++)
        {
            var response = await client.PostAsJsonAsync("/api/auth/login", loginRequest);
            response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
        }

        // 6. denemede rate limit devreye girmeli
        var rateLimitedResponse = await client.PostAsJsonAsync("/api/auth/login", loginRequest);

        // Assert
        rateLimitedResponse.StatusCode.Should().Be(HttpStatusCode.TooManyRequests);
        rateLimitedResponse.Headers.Should().ContainKey("Retry-After");
    }

    [Fact]
    public async Task Api_ShouldReturn200_WhenWithinLimit()
    {
        // Arrange
        var client = _factory.CreateClient();
        client.DefaultRequestHeaders.Add("X-Api-Key", "valid-test-key");

        // Act — limit içinde kal
        for (int i = 0; i < 10; i++)
        {
            var response = await client.GetAsync("/api/products");
            response.StatusCode.Should().Be(HttpStatusCode.OK);
        }
    }
}

Program.cs — Tam Kurulum

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    // 1. Global limiter — tüm isteklere uygulanır
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        if (context.User.Identity?.IsAuthenticated == true)
        {
            var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
            var isPremium = context.User.IsInRole("Premium");

            return RateLimitPartition.GetSlidingWindowLimiter($"user:{userId}",
                _ => new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = isPremium ? 1000 : 100,
                    Window = TimeSpan.FromMinutes(1),
                    SegmentsPerWindow = 6
                });
        }

        var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

        if (ip is "127.0.0.1" or "::1")
            return RateLimitPartition.GetNoLimiter(ip);

        return RateLimitPartition.GetFixedWindowLimiter($"anon:{ip}",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 20,
                Window = TimeSpan.FromMinutes(1)
            });
    });

    // 2. Named politikalar
    options.AddFixedWindowLimiter("auth-strict", p =>
    {
        p.PermitLimit = 5;
        p.Window = TimeSpan.FromMinutes(15);
    });

    options.AddConcurrencyLimiter("heavy-operations", p =>
    {
        p.PermitLimit = 3;
        p.QueueLimit = 2;
    });

    // 3. Rejection handler
    options.OnRejected = async (context, ct) =>
    {
        var logger = context.HttpContext.RequestServices
            .GetRequiredService<ILogger<Program>>();

        logger.LogWarning(
            "Rate limit: {IP} | {Path}",
            context.HttpContext.Connection.RemoteIpAddress,
            context.HttpContext.Request.Path);

        context.HttpContext.Response.StatusCode = 429;

        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)retryAfter.TotalSeconds).ToString();

        await context.HttpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 429,
            Title = "Çok Fazla İstek",
            Detail = "Lütfen daha sonra tekrar deneyin."
        }, ct);
    };
});

var app = builder.Build();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();           // Auth'tan sonra — kullanıcı bilgisi gerekli
app.UseRateLimitHeaders();      // Header'ları ekle
app.MapControllers();

app.Run();

Denetim Listesi

✅ Global limiter — tüm endpoint'ler korunuyor
✅ Auth endpoint'leri için ayrı, sıkı politika var
✅ Ağır operasyonlar (rapor, export) concurrency limiter ile korunuyor
✅ Premium kullanıcılar için daha yüksek limit tanımlandı
✅ İç ağ IP'leri whitelist'e alındı
✅ OnRejected handler — 429 + Problem Details + Retry-After
✅ Retry-After header'ı yanıta ekleniyor
✅ Rate limit bilgisi loglanıyor — monitoring için
✅ Load balanced ortamda Redis backed limiter kullanılıyor
✅ UseRateLimiter() sırası doğru — UseAuthentication() sonrası
✅ Health check endpoint'leri DisableRateLimiting ile muaf
✅ Test senaryoları yazıldı

Temel Çıkarımlar

  • .NET 8 ile harici paket olmadan production-grade rate limiting mümkün
  • Algoritma seçimi senaryoya göre yapılmalı — hepsi için fixed window değil
  • Global limiter tüm endpoint'leri korur, named policy spesifik endpoint'leri özelleştirir
  • Partition key kritik — IP, kullanıcı ID veya API key bazlı sınırlama farklı davranır
  • Load balanced ortamda in-memory limiter yetersizdir — Redis zorunludur
  • Retry-After header'ı istemcilerin akıllı davranmasını sağlar
  • Rate limit aşımları loglanmalı ve izlenmeli — güvenlik sinyali olabilir
  • Health check ve monitoring endpoint'leri her zaman muaf tutulmalıdır

Rate limiting kullanıcıyı cezalandırmaz — sistemi korur. Doğru limitler belirlendiğinde meşru kullanıcılar hiçbir zaman bu sınıra çarpmaz, sadece kötü niyetli veya hatalı istemciler engellenir.