Rate limit trong ASP.NET Core: token, sliding, concurrency
Cách dùng rate limiter sẵn có của ASP.NET Core: fixed window, sliding window, token bucket, concurrency. Khi nào limit theo IP vs user, và cách phân tán qua Redis.
Mục lục
Ngày một client cấu hình sai flood service bằng 10K request/giây là ngày rate limit hết tuỳ chọn. Chương này hướng dẫn cấu hình rate limiter ASP.NET Core, chọn thuật toán đúng cho mỗi use case, và cho thấy phân tán xuyên replica với Redis.
Khi nào rate limit chuyển từ "có thì hay" sang "phải ship"?
Ba tín hiệu.
Service có client ngoài. API public, endpoint webhook, form
public. Mọi thứ attacker, bot, hay tích hợp lỗi có thể đập đều phải
có limit. Default: thoáng cho path bình thường, chặt cho /auth/*
và endpoint write.
Traffic bùng phát. Flash sale nhảy 100x tải bình thường. Không có limiter, downstream (database, payment, queue) hấp thụ đỉnh và có thể sập. Limiter là van back-pressure.
Chi phí scale theo request. Egress cloud, API third-party, tính toán đắt. Một client hỗn xược có thể đẩy hoá đơn qua đêm. Limit theo tenant chặn blast radius.
Nếu service nội bộ, traffic ổn, chi phí cố định, limit là gánh nặng. Phần lớn service .NET hướng public không phải vậy.
Ngân sách số nào cho tier limiter?
Thuật toán Memory/key CPU/check Hành vi burst
Fixed window ~16 byte O(1) cho 2x ở mép
Sliding window ~64 byte O(1) mượt
Token bucket ~24 byte O(1) burst đến kích thước
Concurrency O(N) đang chạy O(1) chặn đồng thời
Qua Redis +network 0.5 ms network mượt, phân tán
Memory mỗi key quan trọng khi có nhiều key (limit theo user trên service triệu user). Sliding window độ chính xác cao có thể đến 1 KB mỗi key. Tune độ chính xác xuống trước khi tune storage lên.
Pipeline limiter trông thế nào?
flowchart LR
Req[Request] --> Edge[Reverse proxy<br/>cap theo IP]
Edge --> App[ASP.NET Core]
App --> RL[Middleware rate limiter]
RL -->|dưới limit| Handler[Handler endpoint]
RL -->|vượt limit| R429[429 Too Many Requests]
Handler --> Down[Service downstream]
Phòng thủ hai tầng. Reverse proxy (Nginx, CloudFront, Azure Front
Door) lo cap DDoS theo IP; application enforce limit nghiệp vụ
theo user hoặc tenant. Middleware chạy trước xác thực cho path
không xác thực và sau cho path đã xác thực - policy RateLimiter
kết hợp với Authorize.
Cấu hình .NET 10 với limiter sẵn có?
builder.Services.AddRateLimiter(opt =>
{
// Default: 100 req/phút theo IP, trả 429
opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
RateLimitPartition.GetTokenBucketLimiter(
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 100,
ReplenishmentPeriod = TimeSpan.FromMinutes(1),
TokensPerPeriod = 100,
AutoReplenishment = true,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
}));
// Policy named chặt hơn cho /auth/login
opt.AddPolicy("auth", ctx =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
}));
// Policy theo user cho path đã xác thực
opt.AddPolicy("per-user", ctx =>
RateLimitPartition.GetTokenBucketLimiter(
partitionKey: ctx.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anon",
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 1000,
ReplenishmentPeriod = TimeSpan.FromMinutes(1),
TokensPerPeriod = 1000,
AutoReplenishment = true
}));
opt.OnRejected = async (ctx, ct) =>
{
ctx.HttpContext.Response.StatusCode = 429;
if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retry))
ctx.HttpContext.Response.Headers.RetryAfter = ((int)retry.TotalSeconds).ToString();
await ctx.HttpContext.Response.WriteAsync("Quá nhiều request.", ct);
};
});
app.UseRateLimiter();
app.MapPost("/auth/login", LoginHandler).RequireRateLimiting("auth");
app.MapGet("/me", MeHandler).RequireAuthorization().RequireRateLimiting("per-user");
Ba chi tiết. PartitionedRateLimiter cho một policy chia theo IP
(hoặc user) không cần viết struct riêng. GlobalLimiter áp trước
khi match route - tốt cho phòng DDoS rộng. Policy named gắn vào
endpoint cụ thể qua RequireRateLimiting.
Phân tán limiter xuyên replica ra sao?
Limiter sẵn là in-memory; ba replica với limit 100/phút cộng lại
cho 300/phút. Để enforce thật cần state chia sẻ - thường là Redis.
// Dùng Microsoft.AspNetCore.RateLimiting.Redis hoặc limiter tuỳ
public class RedisTokenBucket(IConnectionMultiplexer redis)
{
public async Task<bool> TryAcquireAsync(string key, int cost = 1)
{
var script = """
local current = redis.call('GET', KEYS[1])
if not current then current = ARGV[1] else current = tonumber(current) end
if current >= tonumber(ARGV[2]) then
redis.call('SET', KEYS[1], current - ARGV[2], 'EX', ARGV[3])
return 1
else
return 0
end
""";
var result = (long)await redis.GetDatabase().ScriptEvaluateAsync(
script,
new RedisKey[] { $"rl:{key}" },
new RedisValue[] { 100, cost, 60 });
return result == 1;
}
}
Lua script làm check-and-decrement atomic trên Redis. Chương case study lo các lựa chọn thuật toán (token bucket, sliding-window log, sliding-window counter) chi tiết.
Rate limit tạo failure mode nào?
- Default quá chặt - user thật bị 429 trong dùng bình thường. Phòng: instrument limiter qua OpenTelemetry và xem tỉ lệ reject; lặp.
- Bypass qua NAT - hàng nghìn user sau một NAT công ty chia một IP. Limit theo IP khoá hết. Phòng: ưu tiên limit theo user khi đã xác thực; nâng limit theo IP cho proxy đã biết.
- Outage Redis phá limiter - nếu limiter fail-cứng khi Redis lỗi, cả app dừng. Phòng: fail-open trên lỗi với cap in-memory chặt làm phương án an toàn.
- Hot key trên Redis - một user nặng sinh nhiều op limiter làm CPU Redis quá tải. Phòng: shard key limiter hoặc fallback fixed-window local cho key đó.
Khi nào nên bỏ qua rate limit?
Khi service nội bộ, client đã biết, và traffic bị giới hạn bởi limit upstream. Một microservice nội bộ chỉ được gọi bởi sister service đã có rate limit không cần riêng. Thêm limiter ngay khi một third-party (hoặc user) tới được service.
Đi tiếp đâu từ đây?
Bạn đã hoàn tất nhóm foundations, blocks, reliability, ops. Tiếp: các chương case study, bắt đầu với URL shortener - thiết kế đầu cuối đơn giản nhất dùng cache + DB + observability + rate limit trong một service. Sau đó tám case study khác lắp cùng các block thành hệ kiểu Twitter, Uber, Stripe.
Câu hỏi thường gặp
Token bucket hay sliding window thắng?
Theo IP, theo user, hay theo API key?
Sao cần rate limiter phân tán?
Trả status code HTTP nào?
Retry-After đặt khoảng hợp lý. Tuỳ chọn thêm X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset để client biết điều tiết. Đừng drop thầm lặng - client retry và bạn phí cả thời gian hai bên.