All articles

C# Interview Guide: Rate Limiting in .NET 7+ — Fixed Window, Sliding Window, Token Bucket

3 min read
#0
0 reads
0 reading now
0 shares

Q21 — Rate Limiting in .NET 7+ — Fixed Window, Sliding Window, Token Bucket

Level: Senior | Topic: Performance Optimization

Why Rate Limiting?

Rate limiting protects your API from overload — whether from abusive clients, misbehaving integrations, or traffic spikes. .NET 7 introduced first-class built-in rate limiting via Microsoft.AspNetCore.RateLimiting, with four algorithms available out of the box.

The Four Algorithms

  • Fixed Window: Allows N requests per fixed time window. Simple but has a burst spike risk at the window boundary (N requests at the last second + N at the first second of the next window = 2N burst).
  • Sliding Window: Divides the window into segments and tracks requests per segment — eliminates the boundary burst. Higher memory usage.
  • Token Bucket: A bucket of tokens refills at a steady rate. Requests consume a token; if empty, the request is queued or rejected. Allows short bursts up to the bucket size — the most natural fit for API rate limiting.
  • Concurrency Limiter: Limits the number of concurrent in-flight requests (not rate). Ideal for expensive long-running operations like exports or report generation.

Code Example

// Program.cs — full rate limiting setup
services.AddRateLimiter(options =>
{
    // Global — fixed window per IP
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "anon",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit          = 1000,
                Window               = TimeSpan.FromMinutes(1),
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit           = 0
            }));

    // Per-authenticated-user — token bucket (allows short bursts)
    options.AddPolicy("authenticated-api", ctx =>
        RateLimitPartition.GetTokenBucketLimiter(
            partitionKey: ctx.User.Identity?.Name ?? "anon",
            factory: _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit          = 100,   // max burst
                ReplenishmentPeriod = TimeSpan.FromSeconds(1),
                TokensPerPeriod     = 10,    // 10 req/sec average
                AutoReplenishment   = true,
                QueueLimit          = 5
            }));

    // Expensive endpoint — concurrency limit (max 2 concurrent per user)
    options.AddPolicy("export", ctx =>
        RateLimitPartition.GetConcurrencyLimiter(
            partitionKey: ctx.User.Identity?.Name ?? "anon",
            factory: _ => new ConcurrencyLimiterOptions
            {
                PermitLimit = 2,
                QueueLimit  = 1
            }));

    // 429 response with Retry-After header
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = async (ctx, ct) =>
    {
        if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            ctx.HttpContext.Response.Headers.RetryAfter =
                retryAfter.TotalSeconds.ToString("0");

        ctx.HttpContext.Response.StatusCode = 429;
        await ctx.HttpContext.Response.WriteAsync("Rate limit exceeded. Retry later.", ct);
    };
});

app.UseRateLimiter();

// Apply on controller/action
[HttpPost("export"), EnableRateLimiting("export")]
public async Task<IActionResult> ExportData(CancellationToken ct) { ... }

[HttpGet("products"), EnableRateLimiting("authenticated-api")]
public async Task<IActionResult> GetProducts(CancellationToken ct) { ... }

Senior Insight

Always return a Retry-After header on 429 responses — well-behaved clients will back off and retry automatically, reducing thundering herd. Partition by authenticated user ID (not IP) for API-key or JWT-authenticated endpoints — IP-based limiting is easily bypassed and unfairly penalises users behind NAT. Use the concurrency limiter for resource-intensive endpoints rather than a rate limiter — it directly prevents overloading the underlying service regardless of how fast requests arrive.

ShareLinkedInX

Comments

Share your thoughts without signing in.