Vận hành Trung bình 5 phút đọc

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
  1. Khi nào rate limit chuyển từ "có thì hay" sang "phải ship"?
  2. Ngân sách số nào cho tier limiter?
  3. Pipeline limiter trông thế nào?
  4. Cấu hình .NET 10 với limiter sẵn có?
  5. Phân tán limiter xuyên replica ra sao?
  6. Rate limit tạo failure mode nào?
  7. Khi nào nên bỏ qua rate limit?
  8. Đi tiếp đâu từ đây?

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?

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?
Token bucket cho phép burst đến kích thước bucket, sau đó mượt - hợp API có đỉnh hợp lý (user click 'submit' năm lần). Sliding window chặt hơn và công bằng theo thời gian - hợp phòng abuse. Fixed window đơn giản nhất nhưng có vấn đề 'mép cửa sổ'. Concurrency là trục khác: giới hạn request đang chạy, không phải tốc độ request.
Theo IP, theo user, hay theo API key?
Theo principal map đến người trả tiền cho bạn. API public không xác thực thì theo IP - không hoàn hảo vì NAT nhưng là lựa chọn duy nhất. API có xác thực thì theo user (hoặc tenant). API B2B thì theo API key. Xếp tầng: limit cao theo tenant, limit thấp theo user trong đó, limit thấp nhất theo IP cho path không xác thực.
Sao cần rate limiter phân tán?
Vì limiter in-memory của ASP.NET Core đếm chỉ những gì một instance thấy. Ba replica với limit 100 RPS mỗi cái cộng lại cho 300 RPS - vi phạm ý định thật. Rate limiter qua Redis cho counter chia sẻ; case study chương 16 lo thuật toán chi tiết.
Trả status code HTTP nào?
429 Too Many Requests, kèm 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.