Rate limiting API requests with ASP.NET Core and AspNetCoreRateLimit 💻

This is the first post in the series about NuGet packages used to build elmah.io. In this post, I'll introduce you to how we have implemented rate limiting on our API using the AspNetCoreRateLimit package.

When creating a public API, making sure that your API can handle millions of requests without breaking down is an often forgot practice. If you are running on Azure, Cloudflare, and/or similar systems, you get DDoS protection out of the box. But you probably still want to control how many API requests your users can make within a certain time.

The AspNetCoreRateLimit package solves this by implementing rate limiting features directly in the ASP.NET Core pipeline. Rate limiting has built-in support for limiting requests based on the client's IP address, but you can implement your own rules too. The package generally feels very configurable and well done.

AspNetCoreRateLimit is developed by Stefan Prodan and is very active and popular on GitHub.

We already have a range of measures in place to avoid DoS and DDoS attacks. We use AspNetCoreRateLimit to make sure that users don't use their entire message quota in a matter of seconds or minutes if there's a large breakdown on one or more of their websites. We currently allow 500 requests per minute and 3,600 requests per hour. Both limits are enforced for the same API key, why the user can simply generate more keys if wanting to store more log messages.

To set up rate limiting, you need to provide AspNetCoreRateLimit with a range of options. The easiest approach is to store all rules and metrics in memory if you can live with metrics being reset when restarting your ASP.NET Core website. To configure rate limiting to use in-memory persistence, add the following lines to the ConfigureServices method in Startup.cs:

services.AddMemoryCache();
services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();

This will configure the stores needed to implement client-based policies. There are similar stores for implementing rate limiting based on the client's IP, but for this example, we want to implement limiting based on the API key available as an URL parameter in each API request.

To set up the rules, you can either choose appsettings.json or C#. We have chosen C# since we have most of our config in the Startup.cs file anyway. To configure the two rules already mentioned, add the following code to ConfigureServices:

services.Configure<ClientRateLimitOptions>(options =>
{
    options.GeneralRules = new List<RateLimitRule>
    {
        new RateLimitRule
        {
            Endpoint = "*",
            Period = "1m",
            Limit = 500,
        },
        new RateLimitRule
        {
            Endpoint = "*",
            Period = "1h",
            Limit = 3600,
        }
    };
});

This will set up the 500 requests per minute and 3,600 requests per hour that I already mentioned.

The next step is to tell AspNetCoreRateLimit how to group requests. In this example we want to group all requests with the same API key parameter in the same bucket which will require some custom code:

services.AddSingleton<IRateLimitConfiguration, ElmahIoRateLimitConfiguration>();

The ElmahIoRateLimitConfiguration is an implementation of the IRateLimitConfiguration interface that will tell AspNetCoreRateLimit how to group requests:

public class ElmahIoRateLimitConfiguration : RateLimitConfiguration
{
    public ElmahIoRateLimitConfiguration(
        IHttpContextAccessor httpContextAccessor,
        IOptions<IpRateLimitOptions> ipOptions,
        IOptions<ClientRateLimitOptions> clientOptions)
            : base(httpContextAccessor, ipOptions, clientOptions)
    {
    }

    protected override void RegisterResolvers()
    {
        ClientResolvers.Add(new ClientQueryStringResolveContributor(HttpContextAccessor));
    }
}

public class ClientQueryStringResolveContributor : IClientResolveContributor
{
    private IHttpContextAccessor httpContextAccessor;

    public ClientQueryStringResolveContributor(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public string ResolveClient()
    {
        var request = httpContextAccessor.HttpContext?.Request;
        var queryDictionary =
            Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(
                request.QueryString.ToString());
        if (queryDictionary.ContainsKey("api_key")
            && !string.IsNullOrWhiteSpace(queryDictionary["api_key"]))
        {
            return queryDictionary["api_key"];
        }

        return Guid.NewGuid().ToString();
    }
}

The important part is in the ResolveClient method. here we extract the api_key parameter and return that as a string for AspNetCoreRateLimit to use as the group key.

That's it. AspNetCoreRateLimit now returns a status code of 429 when making more than 500 requests per minute or 3,600 requests per hour with the same api_key parameter.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website