Bir API'yi yayına aldığınız gün trafik düzenlidir. Birkaç hafta sonra biri hatalı bir döngüyle saniyede yüzlerce istek atar, bir başkası parola denemeye başlar, bir entegrasyon ortağının betiği ise gece yarısı tüm veriyi çekmeye karar verir. Hız sınırlama (rate limiting), bu durumlarda sistemin geri kalanını korumanın en ucuz yoludur.
.NET 7'den beri ASP.NET Core bunun için yerleşik bir ara katman (middleware) sunuyor. Ek paket gerekmiyor. Bu yazıda algoritmaları, kullanıcı bazlı bölümlemeyi, reddedilen isteğe nasıl yanıt verileceğini ve yerleşik çözümün nerede yetmediğini anlatıyorum.
Neyi, neden sınırlıyoruz?
Hız sınırlamanın üç ayrı amacı olabilir ve hangisini hedeflediğiniz ayarları belirler:
- Koruma: Hatalı veya kötü niyetli bir istemcinin tüm kaynakları tüketmesini önlemek.
- Adalet: Gürültülü bir müşterinin diğerlerinin yanıt süresini bozmasını engellemek.
- Maliyet: Pahalı uç noktaları (raporlama, yapay zekâ çağrıları, e-posta gönderimi) kontrol altında tutmak.
Sınır, güvenlik açığını kapatmaz; yalnızca saldırı maliyetini yükseltir. Kimlik doğrulama ve yetkilendirmenin yerine geçmez, onları tamamlar.
Dört yerleşik algoritma
Katman, System.Threading.RateLimiting üzerine kuruludur ve dört limitleyici sunar:
- Fixed window: Sabit bir pencerede (örneğin dakikada 100) izin verilen istek sayısı. En basit olanıdır; ancak pencere sınırında ani yığılma yaşanabilir. 100 isteği pencerenin son saniyesinde, 100 isteği sonraki pencerenin ilk saniyesinde gönderen istemci kısa sürede 200 istek yapmış olur.
- Sliding window: Pencereyi segmentlere bölüp kaydırır. Yığılma sorununu azaltır, biraz daha fazla bellek kullanır.
- Token bucket: Kova belirli hızda dolar, her istek bir jeton harcar. Kısa süreli patlamalara izin verir, uzun vadede ortalama hızı sınırlar. Çoğu genel API için iyi bir varsayılandır.
- Concurrency: Aynı anda işlenen istek sayısını sınırlar. Hızı değil, eşzamanlılığı kısıtlar; yavaş ve pahalı uç noktalar için uygundur.
Seçimi şöyle yapıyorum: genel API için token bucket, parola denemesi gibi sert sınırlar için fixed veya sliding window, ağır işler için concurrency.
Temel kurulum
Politikalar AddRateLimiter ile tanımlanır, UseRateLimiter ile devreye alınır:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddTokenBucketLimiter("genel", o =>
{
o.TokenLimit = 50;
o.TokensPerPeriod = 10;
o.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
o.QueueLimit = 0;
o.AutoReplenishment = true;
});
options.AddConcurrencyLimiter("agir-is", o =>
{
o.PermitLimit = 4;
o.QueueLimit = 10;
});
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/urunler", () => Results.Ok())
.RequireRateLimiting("genel");
app.MapPost("/raporlar", () => Results.Accepted())
.RequireRateLimiting("agir-is");
app.Run();
QueueLimit = 0 istek sınırı aşınca hemen reddedildiği anlamına gelir. Kuyruk büyütmek isteği bekletir; bu, kullanıcı için daha yumuşaktır ama bekleyen istekler de kaynak tutar. Genel API'lerde sıfır, kısa süreli yoğunluk beklenen uç noktalarda küçük bir kuyruk makul bir başlangıçtır.
Herkesi aynı kovaya koymayın: bölümleme
Yukarıdaki politikalar tüm istemciler için tek bir sayaç tutar. Bir kullanıcı sınırı doldurursa herkes 429 alır. Bunu önlemek için isteği bir anahtara göre bölümlemek gerekir:
options.AddPolicy("kullanici-bazli", httpContext =>
{
var anahtar =
httpContext.User.Identity?.IsAuthenticated == true
? $"user:{httpContext.User.FindFirst("sub")?.Value}"
: $"ip:{httpContext.Connection.RemoteIpAddress}";
return RateLimitPartition.GetTokenBucketLimiter(anahtar, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = 30,
TokensPerPeriod = 5,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
QueueLimit = 0
});
});
Anahtar seçimi önemlidir. Kimliği doğrulanmış kullanıcıda kullanıcı veya kiracı kimliği en adil anahtardır. Anonim isteklerde IP adresine düşmek zorunda kalırız; ortak bir ağın (ofis, mobil operatör) arkasındaki birçok kullanıcı aynı IP'yi paylaşabilir. Bu yüzden anonim sınırı, giriş yapmış kullanıcılara göre daha cömert tutmak gerekebilir.
Anahtar bir proxy veya yük dengeleyici arkasında RemoteIpAddress ile alınıyorsa, doğru yapılandırılmış ForwardedHeaders ara katmanı olmadan herkes proxy'nin IP'si gibi görünür ve tüm trafik tek kovaya düşer. Tersine, X-Forwarded-For başlığına koşulsuz güvenirseniz saldırgan her istekte farklı bir değer göndererek sınırı aşar. Yalnızca bildiğiniz proxy'lerden gelen başlığa güvenin.
Reddedilen isteğe iyi yanıt vermek
Varsayılan 429 yanıtı gövdesizdir. İstemciyi ne zaman tekrar deneyeceği konusunda bilgilendirmek, hem kullanıcı deneyimini hem de gereksiz tekrar yükünü iyileştirir:
options.OnRejected = async (context, ct) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var bekleme))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)bekleme.TotalSeconds).ToString();
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
hata = "cok_fazla_istek",
mesaj = "İstek sınırına ulaştınız. Lütfen biraz sonra tekrar deneyin."
}, ct);
};
Retry-After başlığı standart bir sözleşmedir ve iyi yazılmış istemciler buna uyar. Her limitleyici bu meta veriyi üretmez; örneğin concurrency limitleyicide bekleme süresi bilinmez, bu yüzden başlığı koşullu yazıyoruz.
İstemci tarafında da tarafınızı tutun: kendi SDK veya ön yüzünüz 429 aldığında hemen yeniden denememeli, Retry-After süresini beklemeli ve üstel geri çekilme (exponential backoff) ile rastgele sapma (jitter) uygulamalıdır. Aksi hâlde sınır, tekrar deneme fırtınasını besler.
Kritik uç noktalar için ayrı, sert sınırlar
Tüm uç noktalar aynı değerde değildir. Giriş, parola sıfırlama ve kayıt uç noktaları, kaba kuvvet denemelerine karşı çok daha sıkı sınır ister:
options.AddPolicy("giris", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
$"login:{httpContext.Connection.RemoteIpAddress}",
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
}));
Burada yalnızca IP'ye bakmak yetmez; hedef hesap başına da sayaç tutmak gerekir, çünkü saldırgan binlerce IP kullanabilir. Hesap bazlı kilitleme ve uyarı mantığı genellikle uygulama katmanında ayrıca kurulur.
Yerleşik çözümün sınırları
Yerleşik limitleyiciler süreç belleğinde çalışır. Bu birkaç önemli sonuç doğurur:
- Çok örnekli dağıtım: API'nin üç kopyası varsa her kopya kendi sayacını tutar. Efektif sınır, kopya sayısıyla çarpılır. Yük dengeleyici istekleri eşit dağıtıyorsa bu çoğu zaman kabul edilebilir bir yaklaşıklıktır; kesin sınır gerekiyorsa Redis gibi paylaşılan bir depoya dayanan bir çözüm veya API gateway düzeyinde sınırlama gerekir.
- Yeniden başlatma: Sayaçlar bellekte olduğundan dağıtımda sıfırlanır.
- Katman sırası: Sınırlama ne kadar erkense, kötü isteğe harcanan kaynak o kadar azdır. Ancak kullanıcı bazlı anahtar kullanıyorsanız
UseRateLimiter, kimlik doğrulama ara katmanından sonra gelmelidir kiHttpContext.Userdolu olsun.
Kaba bir ilk savunmayı ters proxy veya gateway'de, ince ve iş kurallarına bağlı sınırları ise uygulamada tutmak iyi bir işbölümüdür.
Test etmek ve gözlemlemek
Sınırı koyduktan sonra davranışını doğrulamak gerekir. Entegrasyon testinde sınırın hemen üstünde istek atıp ilk reddin tam beklenen noktada geldiğini, Retry-After başlığının bulunduğunu ve farklı kullanıcıların birbirini etkilemediğini kontrol edin.
Üretimde ise 429 sayısını bir metrik olarak izleyin. Ani artış ya bir saldırıyı ya da çok sıkı koyduğunuz bir sınırı gösterir. Gerçek kullanıcılar 429 görüyorsa sınır hatalıdır; bunu önce kullanım verisinden okuyun, sonra değeri ayarlayın.
Kapanış
Hız sınırlama sihirli bir kalkan değil, kapasite planının bir parçasıdır. Önce hangi kaynağı korumak istediğinizi belirleyin, algoritmayı buna göre seçin, kullanıcı bazlı bölümleyin, reddi anlaşılır bir yanıtla bildirin ve çok örnekli ortamın getirdiği yaklaşıklığı bilerek kabul edin. Yerleşik ara katman bu işin büyük kısmını birkaç satırda çözer; geri kalanı doğru anahtarı seçmek ve davranışı ölçmektir.