Search code examples
c#.netasp.net-corerate-limiting.net-7.0

.NET 7 Rate Limiting - Rate limit by IP address


The title is pretty self explanatory. How do I rate limit the following limiter by IP address? In other words, each IP address is able to do 2 requests per 10 seconds.

app.UseRateLimiter(new RateLimiterOptions
    {
        OnRejected = (context, _) =>
        {
            if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            {
                context.HttpContext.Response.Headers.RetryAfter =
                    ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);

                app.Logger.LogWarning("Rate limit exceeded, retry after {RetryAfter} seconds", retryAfter.TotalSeconds);
            }

            context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

            return new ValueTask();
        }
    }
    // You're allowed 2 requests per 10 seconds.
    .AddFixedWindowLimiter("fixed",
        new FixedWindowRateLimiterOptions(2,
            window: TimeSpan.FromSeconds(10),
            queueProcessingOrder: QueueProcessingOrder.OldestFirst,
            queueLimit: 0,
            autoReplenishment: true)));

app.MapControllers().RequireRateLimiting("fixed");

Solution

  • Using a PartitionedRateLimiter is the way to go here:

    PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            httpContext.Request.Headers.TryGetValue("X-Forwarded-For", out StringValues forwardedFor);
            string? ipAddress = forwardedFor.FirstOrDefault() ?? httpContext.Request.HttpContext.Connection.RemoteIpAddress?.ToString();
            if (ipAddress == null) return RateLimitPartition.GetNoLimiter("none");
    
            return RateLimitPartition.GetTokenBucketLimiter(ipAddress, (key) =>
            {
                return new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 100,
                    ReplenishmentPeriod = TimeSpan.FromMinutes(15),
                    TokensPerPeriod = 25
                };
            });
        });
    

    The tricky part is getting the IP address. As noted in the comments, when behind a reverse proxy or API gateway, double check the IP is forwarded in the correct header (in this example it is X-Forwarded-For). Else you will probably rate-limit your reverse proxy, which is not what you want. I use nginx as a reverse proxy, and the following configuration will always populate the X-Forwarded-For header to your app:

        # inside nginx reverse proxy config file
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    

    That being said, there are some issues with IP rate limiting. Some home-network providers will use NAT even with IPv4, as do some cellular network providers - so potential thousands of users using the same IPv4. Additionally, when you use IPv6, you might want to modify the code to use the whole /64 or even /48 subnet of the source host as a partion key.