In this tutorial, we'll learn how to build a sliding window rate limiter for ASP.NET Core using Redis.
The pattern that we are implementing here is a sliding window rate limiter. A sliding window rate limiter, unlike a fixed window, restricts requests for a discrete window prior to the current request under evaluation. As opposed to a fixed window rate limiter which groups the requests into a bucket based on a very definitive time window. For example, if you have a 10 req/minute rate limiter, on a fixed window, you could encounter a case where the rate-limiter allows 20 requests inside of a minute. That's because if first 10 requests are on the left hand side of the current window, and the next 20 requests are on the right hand side of the window, both have enough space in their respective buckets to be allowed through. If you sent those same 20 requests through a sliding window limited rate limiter on the other hand, if they are all sent within 60 seconds of each other, only 10 will make it through. Using Sorted Sets and Lua scripts, implementing one of these rate limiters is a breeze.
- Must have the .NET 5+ SDK installed
- Some way of running Redis, for this tutorial we'll use Docker Desktop
- IDE for writing C# VS Code, Visual Studio, or Rider
Before we begin, startup Redis. For this example, we'll use the Redis docker image:
In your terminal, navigate to where you want the app to live and run:
Cd into the
SlidingWindowRateLimiter folder and run the command
dotnet add package StackExchange.Redis.
SlidingWindowRateLimiter.csproj in Rider, Visual Studio, or open the folder in VS Code. In the
Controllers folder, add an API controller called
RateLimitedController, when all this is complete,
RateLimitedController.cs should look like the following:
To use Redis, we're going to initialize an instance of the ConnectionMultiplexer from
StackExchange.Redis, to do so, go to the
ConfigureServices method inside of
Startup.cs and add the following line:
RateLimitedController.cs inject the ConnectionMultiplexer into the controller and pull out an
IDatabase object from it with the following:
We will add a simple route that we will Rate Limit; it will be a POST request route on our controller. This POST request will use Basic auth - this means that each request is going to expect a header of the form
Authorization: Basic <base64encoded> where the
base64encoded will be a string of the form
apiKey:apiSecret base64 encoded, e.g.
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==. This route will parse the key out of the header and return an OK result.
With that setup, you should run the project with a
dotnet run, and if you issue a POST request to
https://localhost:5001/api/RateLimited/sliding - with apiKey
foobar and password
password, you will get a 200 OK response back.
You can use this cURL request to elicit that response:
To implement this pattern we will need to do the following:
- The client will create a key for the server to check, this key will be of the format
- That key will map to a sorted set in Redis, we will check the current time, and shave off any requests in the sorted set that are outside of our window
- We will then check the cardinality of the sorted set
- If the cardinality is less than our limit, we will
- Add a new member to our sorted set with a score of the current time in seconds, and a member of the current time in microseconds
- Set the expiration for our sorted set to the window length
- return 0
- If the cardinality is greater than or equal to our limit we will return 1
The trick here is that everything needs to happen atomically, we want to be able to trim the set, check its cardinality, add an item to it, and set it's expiration, all without anything changing in the interim. Fortunately this is a perfect place to use a Lua Script. Specifically we are going to be using the StackExchange script preparation engine to drive our lua script, meaning we can use
@variable_name in place of a particular position in
KEYS in the script. Our Lua script will be:
In order to use that in our app, we will create a new static class called
Scripts which will hold the text of the script, and prepare the script to run with
StackExchange.Redis. Create a new file called
Scripts.cs and add the following to it.
Back in our
RateLimitedController Sliding method, we will add a few lines of code to check if we should throttle the API request, replace the return statement with the following:
The whole method should look like this now:
Now, if we start our server back up with
dotnet run and try running the following command:
You will see some of your requests return a
200, and 10 will return a
429. If you wait for some and run the above command again you may see some behavior where every other request goes through. That's because the window slides every second and only the previous 30 seconds requests are considered when determining whether to throttle the request. The above command the first time will produce an output something like this:
- You can find the code used for this tutorial in GitHub