ASP.NET Core cookbook · Intermediate

Rate limit per user and per IP

A single client can exhaust the API, and login endpoints are brute-forced.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Use partitioned rate limiters: per user (or API key) for authenticated traffic, per IP for anonymous traffic, and a strict policy for login. Return 429 with Retry-After.

Solution

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = (ctx, _) =>
    {
        if (ctx.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            ctx.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
        return ValueTask.CompletedTask;
    };
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(http =>
    {
        var user = http.User.Identity?.IsAuthenticated == true ? http.User.Identity.Name : null;
        return user is not null
            ? RateLimitPartition.GetTokenBucketLimiter($"user:{user}", _ => new TokenBucketRateLimiterOptions
                { TokenLimit = 100, TokensPerPeriod = 50, ReplenishmentPeriod = TimeSpan.FromSeconds(10) })
            : RateLimitPartition.GetFixedWindowLimiter($"ip:{http.Connection.RemoteIpAddress}", _ => new FixedWindowRateLimiterOptions
                { PermitLimit = 60, Window = TimeSpan.FromMinutes(1) });
    });
    options.AddPolicy("login", http => RateLimitPartition.GetSlidingWindowLimiter(
        $"login:{http.Connection.RemoteIpAddress}", _ => new SlidingWindowRateLimiterOptions
            { PermitLimit = 5, Window = TimeSpan.FromMinutes(1), SegmentsPerWindow = 6 }));
});
var app = builder.Build();
app.UseRateLimiter();
app.MapPost("/login", () => Results.Ok()).RequireRateLimiting("login");
app.Run();

Pitfalls

  • Using RemoteIpAddress behind a proxy without UseForwardedHeaders (everyone shares the proxy IP).
  • In-process limits with several instances; each instance allows the full rate.
  • Unbounded partition keys from user input (headers) that grow memory.

Variations

  • Apply global limits at the API gateway and keep per-endpoint limits in the app.
  • [EnableRateLimiting("policy")] and [DisableRateLimiting] work on controllers too.

Further reading: Rate limiting middleware

Reference: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit

More ASP.NET Core recipes

Was this page helpful?