Rate limiting is a technique that limits the number of requests in a system, usually within a specific time frame. It can be used with both APIs and websites. Why would you need that?
There are different goals that you can achieve with it:
- Reduction of the unnecessary server load. When the server is not overloaded, you can ensure better performance and reliability.
- Usage limits. If clients are charged per request, rate limiting can help you to make sure that they are using only the resources they have paid for.
- Protection against DDoS attacks. This technique can successfully block excessive traffic attempts that drive server corruption.
- Prevention of unnecessary resource usage. This can guarantee performance and cost optimization.
Rate Limiting Algorithms
Here are 4 common types of rate limiting algorithms that can be used to control the flow of requests.
- Fixed window limiter. This algorithm presupposes that there is a fixed time window to limit requests. For example, we don’t allow more than 10 requests per minute. After that period, the timer resets and a new time begins.
- Concurrency limiter. This algorithm is easier to use than the previous one. The time parameter is not involved. We can just limit the number of concurrent requests.
- Sliding window limiter. Instead of having a static long time window, we can divide it into multiple time segments. Unlike fixed window rate limiting, the sliding window approach offers smoother handling of requests near the boundary of a time window. When a segment expires, the requests from the previous segment carry over to the current one.
- Token bucket limiter. It is similar to the sliding window limiter. But instead of adding back the requests taken from the expired segment, a fixed number of them are added after each specified period, mentioned as the replenishment period.
Stock Market API
Let’s take a look at a simple stock market API scenario to analyze how rate limiting works. In the beginning, we have a .NET Core 8 API application that has a single endpoint providing stock market data.
Fixed Window Limiter
Let’s imagine that we have launched a new app and need to attract the attention of new clients to it. That’s why the API will be publicly available to everyone. However, we don’t want to exceed our budget limits. That’s why we will implement the fixed window limiter algorithm to restrict access by IP Address.
Starting from .NET 7 and over, there is a built-in rate limiting middleware in the Microsoft.AspNetCore.RateLimiting namespace.
The implementation of a rate limiting policy will be quite simple. This process will involve the registration of rate limiting services and the enabling of the middleware.
We will use RateLimitPartition to apply the policy per user IP address rather than globally.
| 1234567891011 | builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed-by-ip", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100, // Limit to 100 requests
Window = TimeSpan.FromHours(1), // Per 1-hour window
}));
});
|
The RateLimitPartition will ensure that rate limits are applied individually to each client based on their IP address. The FixedWindowRateLimiterOptions will allow us to configure the request limit and window duration.
After registering the rate limiter service, we will enable the rate limiting middleware. Remember that the order of the middleware registration matters, so call it before your minimal API endpoints or controllers.
All API consumers have no more than 100 requests per hour. But how can we apply it in a minimal API endpoint?
It is possible to do it by calling RequireRateLimiting with the fixed-by-ip policy name:
| 123456789101112 | app.MapGet("/api/data", () =>
{
var stockMarketData = new List<object>
{
new { Symbol = "AAPL", Price = 175.64, Change = +1.23 },
new { Symbol = "GOOGL", Price = 2843.66, Change = -15.44 },
new { Symbol = "AMZN", Price = 3491.15, Change = +23.89 },
new { Symbol = "TSLA", Price = 753.42, Change = -5.76 }
};
return Results.Json(stockMarketData);
}).RequireRateLimiting("fixed-by-ip");
|
After testing the hourly limit, the user will receive an HTTP 503 (Service Unavailable) error, which doesn’t clearly communicate the case. To improve the user experience, we will modify the rejection status code to 429 (Too Many Requests).
| 123456789101112 | builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("fixed-by-ip", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 100, // Limit to 100 requests
Window = TimeSpan.FromHours(1), // Per 1-hour window
}));
});
|
Sliding Window Limiter
With the previous implementation, users can run out of all available requests at once and be blocked until the window resets. To offer them an enhanced user experience, we will use a sliding window limiter that distributes requests more evenly across the time window.
| 12345678910111213141516 | builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("sliding-by-ip", httpContext =>
RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 100, // Limit to 100 requests
Window = TimeSpan.FromHours(1), // 1-hour window
SegmentsPerWindow = 6, // 6 segments (10 minutes each)
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 20 // Allow up to 20 queued requests
}));
});
|
The 1-hour window will be divided into 6 segments of 10 minutes each. If users make many requests in one interval, they will not immediately regain full access after the window resets.
Instead, requests will be gradually provided as time moves forward.
With QueueProcessingOrder and QueueLimit, requests will be queued if the limit exceeds. They will be processed in a First-In-First-Out (FIFO) manner, with 20 additional requests to be queued after the limit is hit.
Let’s apply the rate limiting policy to our endpoint:
| 1234567891011 | app.MapGet("/api/data", () =>
{
var stockMarketData = new List<object>
{
new { Symbol = "AAPL", Price = 175.64, Change = +1.23 },
new { Symbol = "GOOGL", Price = 2843.66, Change = -15.44 },
new { Symbol = "AMZN", Price = 3491.15, Change = +23.89 },
new { Symbol = "TSLA", Price = 753.42, Change = -5.76 }
};
return Results.Json(stockMarketData);
}).RequireRateLimiting("sliding-by-ip");
|
Token Bucket Limiter
As the traffic grows, we want to give some further flexibility to users. Instead of just adding the requests for the expired segment, the Token Bucket algorithm adds a fixed number of tokens each replenishment period.
| 1234567891011121314151617 | builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("token-bucket-by-ip", httpContext =>
RateLimitPartition.GetTokenBucketLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 20, // Maximum of 20 requests at a time
ReplenishmentPeriod = TimeSpan.FromMinutes(10), // Replenish requests every 10 minutes
TokensPerPeriod = 20, // Add 20 requests every 10 minutes
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 5, // Allow up to 5 queued requests,
AutoReplenishment = true // Automatically add requests at the replenishment period
}));
});
|
Every 10 minutes, 20 requests are replenished. This means users can burst up to 20 requests after waiting for the replenishment period to pass, with QueueLimit of 5.
Public and Premium Users
The demand for the product increases over time. Sooner or later it will be high time to monetize it. We can create a login system to identify users and assign a role to each of them based on the chosen plan. Public users will have a limit of 20 requests per 10 minutes, while premium users will have 40 requests per 10 minutes. To do this, we will use the token bucket algorithm.
Let’s differentiate roles and give more requests to premium users:
| 1234567891011121314151617181920212223242526272829303132333435363738 | builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("token-bucket-by-role", httpContext =>
{
var role = httpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value ?? "Publi
c";
if (role == "Premium")
{
return RateLimitPartition.GetTokenBucketLimiter(
partitionKey: role,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 40, // Premium users get 40 requests per 10 minutes
ReplenishmentPeriod = TimeSpan.FromMinutes(10),
TokensPerPeriod = 40,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 10,
AutoReplenishment = true
});
}
else
{
return RateLimitPartition.GetTokenBucketLimiter(
partitionKey: role,
factory: _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 20, // Public users get 20 requests per 10 minutes
ReplenishmentPeriod = TimeSpan.FromMinutes(10),
TokensPerPeriod = 20,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 5,
AutoReplenishment = true
});
}
});
});
|
The partition key is assigned by role now, which is extracted from the claims using ClaimTypes.Role. If the role is missing or invalid, it defaults to Public.
Let’s change the policy one more time:
| 12345678910111213 | app.MapGet("/api/data", () =>
{
var stockMarketData = new List<object>
{
new { Symbol = "AAPL", Price = 175.64, Change = +1.23 },
new { Symbol = "GOOGL", Price = 2843.66, Change = -15.44 },
new { Symbol = "AMZN", Price = 3491.15, Change = +23.89 },
new { Symbol = "TSLA", Price = 753.42, Change = -5.76 }
};
return Results.Json(stockMarketData);
}).RequireRateLimiting("token-bucket-by-role");
|
Conclusion
With a well-structured plan, it is quite straightforward to implement rate limiting in your NET API. In our example with the Stock Market API, we covered 3 algorithms using the built-in middleware. This way, you can not only protect your product from excessive load but also create specific use cases for monetization.
If you need any help in working with your ASP.NET Core projects, do not hesitate to contact us. At Softacom, we have solid expertise in using this technology for building software products of different types and complexity. Schedule a free consultation with our specialists to learn more about our experience and services.