Modern web APIs must handle unpredictable traffic patterns. Whether caused by legitimate traffic spikes, aggressive clients, or malicious attacks, excessive requests can overwhelm an application, degrade performance, and impact other users.
Rate limiting is a critical technique for protecting APIs by controlling how many requests a client can make within a given period. ASP.NET Core includes built-in rate limiting middleware that provides flexible and high-performance request throttling without requiring third-party libraries.
In this article, you'll learn how to implement production-ready rate limiting in ASP.NET Core 11, explore different rate limiting algorithms, and understand how to evaluate their effectiveness using a structured testing methodology.
Note: This article focuses on implementation and testing methodology. It does not include fabricated benchmark results.
Why Rate Limiting Matters
Without rate limiting, applications may experience:
API abuse
Denial-of-service attempts
Excessive resource consumption
Database overload
Unfair resource usage
Increased infrastructure costs
Rate limiting helps ensure fair access while protecting backend services.
Common Rate Limiting Algorithms
ASP.NET Core supports multiple algorithms.
| Algorithm | Best For | Characteristics |
|---|
| Fixed Window | Simple APIs | Easy to configure |
| Sliding Window | General APIs | Smoother traffic distribution |
| Token Bucket | Burst traffic | Allows temporary bursts |
| Concurrency Limiter | Long-running requests | Limits simultaneous requests |
Choosing the appropriate algorithm depends on your workload.
Create a Web API
dotnet new webapi -n RateLimitingDemo
The built-in rate limiting middleware is available through ASP.NET Core.
Configure Rate Limiting
Register the service.
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", config =>
{
config.PermitLimit = 100;
config.Window = TimeSpan.FromMinutes(1);
config.QueueLimit = 20;
config.QueueProcessingOrder =
QueueProcessingOrder.OldestFirst;
});
});
This configuration allows 100 requests per minute while queuing a limited number of additional requests.
Enable the Middleware
var app = builder.Build();
app.UseRateLimiter();
app.MapControllers();
app.Run();
The middleware should be added before endpoints that require rate limiting.
Apply a Policy
Apply the configured policy to an endpoint.
app.MapGet("/products", () =>
{
return Results.Ok("Success");
})
.RequireRateLimiting("api");
Only requests matching the configured limits are processed.
Fixed Window Limiter
A fixed window counts requests during a defined time interval.
Example:
Minute 1
██████████ 100 Requests
Minute 2
Counter Reset
It is simple and efficient but may allow bursts near window boundaries.
Sliding Window Limiter
Configure a sliding window.
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("sliding", config =>
{
config.PermitLimit = 100;
config.Window = TimeSpan.FromMinutes(1);
config.SegmentsPerWindow = 6;
});
});
Instead of resetting all counters at once, the sliding window gradually expires older requests.
This produces smoother traffic handling.
Token Bucket Limiter
Configure a token bucket.
builder.Services.AddRateLimiter(options =>
{
options.AddTokenBucketLimiter("token", config =>
{
config.TokenLimit = 100;
config.TokensPerPeriod = 20;
config.ReplenishmentPeriod =
TimeSpan.FromSeconds(10);
});
});
The token bucket algorithm allows short bursts while maintaining a controlled average request rate.
Concurrency Limiter
Limit simultaneous request execution.
builder.Services.AddRateLimiter(options =>
{
options.AddConcurrencyLimiter("concurrent", config =>
{
config.PermitLimit = 25;
config.QueueLimit = 50;
});
});
This strategy is particularly useful for expensive operations such as file uploads or report generation.
Rate Limiting by Client
Policies can be partitioned using a client identifier.
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("clients", context =>
RateLimitPartition.GetFixedWindowLimiter(
context.Connection.RemoteIpAddress?.ToString()
?? "unknown",
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 50,
Window = TimeSpan.FromMinutes(1)
}));
});
Other partition keys may include:
API key
User ID
Tenant ID
JWT claim
Choose a partition strategy that aligns with your authentication model.
Custom Rejection Response
Customize responses for rejected requests.
builder.Services.AddRateLimiter(options =>
{
options.OnRejected = async (context, _) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync(
"Too many requests.");
};
});
Returning HTTP 429 clearly communicates that the client has exceeded the configured limit.
End-to-End Request Flow
A typical request follows these steps:
Client sends a request.
Rate limiter identifies the client partition.
Available permits are checked.
If permitted, the request proceeds.
If the limit is exceeded, HTTP 429 is returned.
Permits are replenished according to the configured algorithm.
This protects downstream services from excessive traffic.
Algorithm Comparison
| Feature | Fixed Window | Sliding Window | Token Bucket | Concurrency |
|---|
| Easy to configure | Yes | Moderate | Moderate | Yes |
| Handles bursts | Limited | Good | Excellent | No |
| Fair request distribution | Moderate | Excellent | Good | Good |
| Limits concurrent work | No | No | No | Yes |
| Best for APIs | Yes | Yes | Yes | Specific workloads |
Testing Methodology
The research brief emphasizes production readiness but does not include benchmark results. To evaluate your implementation:
Test Environment
Maintain consistency for:
.NET SDK version
Hardware
Operating system
API configuration
Rate limiting policy
Test Scenarios
Evaluate:
Single client
Multiple clients
Burst traffic
Sustained traffic
Concurrent requests
Mixed request patterns
Metrics to Measure
Collect:
Useful Tools
Useful tools include:
k6
Bombardier
Apache JMeter
dotnet-counters
dotnet-trace
ASP.NET Core logging
Use production-like traffic patterns instead of synthetic single-user tests.
Best Practices
Apply rate limiting to public APIs.
Partition limits by authenticated identity whenever possible.
Return HTTP 429 for rejected requests.
Monitor rejection rates.
Use sliding window or token bucket for smoother traffic handling.
Combine rate limiting with authentication and caching.
Document API limits for consumers.
Review limits regularly as traffic grows.
Common Mistakes
| Mistake | Impact |
|---|
| Applying one global limit to every client | Unfair resource allocation |
| Setting limits too aggressively | Legitimate requests rejected |
| Ignoring HTTP 429 handling | Poor client experience |
| Using IP-based limits behind proxies without configuration | Incorrect client identification |
| Not monitoring rejection metrics | Hidden traffic issues |
| Choosing the wrong algorithm | Reduced effectiveness |
Troubleshooting
Legitimate Users Receive HTTP 429
Review:
Permit limits
Window duration
Client partitioning
Traffic patterns
Increase limits only after confirming genuine demand.
Rate Limiting Doesn't Apply
Verify:
UseRateLimiter() is registered.
The correct policy name is used.
Endpoints require the configured policy.
Traffic Spikes Still Affect Performance
Check:
Algorithm selection
Queue limits
Downstream bottlenecks
Database capacity
Rate limiting protects the application but does not replace efficient application design.
FAQs
Which rate limiting algorithm should I choose?
For most APIs, the sliding window algorithm provides a good balance between fairness and performance. Token bucket is well suited for workloads that require controlled bursts.
Does rate limiting improve security?
It helps mitigate abuse and certain denial-of-service scenarios, but it should be combined with authentication, authorization, firewalls, and other security measures.
Should rate limits be based on IP addresses?
IP-based limits work for anonymous APIs, but authenticated applications often benefit from partitioning by user ID, API key, or tenant.
What HTTP status code should be returned?
The standard response is 429 Too Many Requests.
Can different endpoints have different limits?
Yes. ASP.NET Core allows multiple named policies that can be applied independently to different endpoints or controllers.
Conclusion
Rate limiting is an essential component of building resilient and scalable ASP.NET Core APIs. By controlling request rates, protecting backend resources, and ensuring fair access, it helps applications remain responsive even during traffic spikes or abusive usage.
ASP.NET Core's built-in rate limiting middleware provides flexible algorithms, straightforward configuration, and seamless integration with modern APIs. Combined with proper monitoring and realistic load testing, it enables you to build production-ready services that can handle growing traffic confidently.