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:

Rate limiting helps ensure fair access while protecting backend services.

Common Rate Limiting Algorithms

ASP.NET Core supports multiple algorithms.

AlgorithmBest ForCharacteristics
Fixed WindowSimple APIsEasy to configure
Sliding WindowGeneral APIsSmoother traffic distribution
Token BucketBurst trafficAllows temporary bursts
Concurrency LimiterLong-running requestsLimits 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:

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:

  1. Client sends a request.

  2. Rate limiter identifies the client partition.

  3. Available permits are checked.

  4. If permitted, the request proceeds.

  5. If the limit is exceeded, HTTP 429 is returned.

  6. Permits are replenished according to the configured algorithm.

This protects downstream services from excessive traffic.

Algorithm Comparison

FeatureFixed WindowSliding WindowToken BucketConcurrency
Easy to configureYesModerateModerateYes
Handles burstsLimitedGoodExcellentNo
Fair request distributionModerateExcellentGoodGood
Limits concurrent workNoNoNoYes
Best for APIsYesYesYesSpecific workloads

Testing Methodology

The research brief emphasizes production readiness but does not include benchmark results. To evaluate your implementation:

Test Environment

Maintain consistency for:

Test Scenarios

Evaluate:

Metrics to Measure

Collect:

Useful Tools

Useful tools include:

Use production-like traffic patterns instead of synthetic single-user tests.

Best Practices

Common Mistakes

MistakeImpact
Applying one global limit to every clientUnfair resource allocation
Setting limits too aggressivelyLegitimate requests rejected
Ignoring HTTP 429 handlingPoor client experience
Using IP-based limits behind proxies without configurationIncorrect client identification
Not monitoring rejection metricsHidden traffic issues
Choosing the wrong algorithmReduced effectiveness

Troubleshooting

Legitimate Users Receive HTTP 429

Review:

Increase limits only after confirming genuine demand.

Rate Limiting Doesn't Apply

Verify:

Traffic Spikes Still Affect Performance

Check:

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.