When a client submits too many queries in a short amount of time, an API may become sluggish or unavailable. A poorly designed application, an automated script, an unexpected surge in traffic, or a deliberate attempt to overwhelm the service can all cause this. Rate restriction aids in regulating the frequency of client access to an API. Developers may create rules that restrict requests based on time windows, concurrent requests, or other permitted criteria using ASP.NET Core's built-in rate limitation middleware.

Setting a request restriction is simply one aspect of the approach, though. When restrictions are exceeded, the policy must reflect the usage patterns of the API, identify clients effectively, provide helpful replies, and function well across many application instances.

This article describes how to apply rules to endpoints, return HTTP 429 replies, establish rate limitation in ASP.NET Core, and steer clear of typical implementation errors.

What Is Rate Limiting?
Rate limiting controls the number of requests allowed within a defined period or the number of requests that can execute concurrently. For example, an API might allow a client to make 100 requests per minute. If the client exceeds that limit, the API can reject additional requests until the applicable limit permits more traffic.

Rate limiting can help protect APIs from excessive usage, but it does not replace authentication, authorization, input validation, or network-level protection.

ASP.NET Core provides several built-in rate-limiting algorithms.

Algorithm

How it works

Typical use

Fixed window

Allows a configured number of requests during each time window

Simple request quotas

Sliding window

Distributes the limit across overlapping time segments

Smoother request limits

Token bucket

Refills tokens over time and allows controlled bursts

APIs that need limited burst capacity

Concurrency limiter

Limits the number of requests executing simultaneously

Expensive or resource-intensive operations

The first three algorithms limit request frequency over time. A concurrency limiter controls simultaneous operations rather than enforcing a requests-per-minute quota.

Choosing the right algorithm depends on the endpoint and the resources it consumes.

Add Rate Limiting Middleware

ASP.NET Core includes rate limiting middleware through the Microsoft.AspNetCore.RateLimiting namespace. In modern ASP.NET Core applications, you can configure it in Program.cs.

The following example uses a fixed-window policy:
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode =
        StatusCodes.Status429TooManyRequests;

    options.AddFixedWindowLimiter("api", limiter =>
    {
        limiter.PermitLimit = 100;
        limiter.Window = TimeSpan.FromMinutes(1);
        limiter.QueueLimit = 0;
        limiter.AutoReplenishment = true;
    });
});

var app = builder.Build();

app.UseRouting();

app.UseRateLimiter();

app.MapControllers();

app.Run();


This configuration creates a policy named api. It allows up to 100 requests per minute for each limiter partition. Requests that exceed the limit are rejected with HTTP 429 because the queue limit is zero.The example defines the policy but does not automatically apply it to every endpoint. You must attach the policy to the endpoints that should use it.

The middleware should run after routing when endpoint-specific rate-limiting policies are being applied. If authentication is part of the design, place authentication and authorization middleware in an order that allows the limiter to use the appropriate authenticated identity.

Apply a Rate-Limiting Policy to a Controller

For a controller-based API, use the [EnableRateLimiting] attribute to apply a named policy.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

[ApiController]
[Route("api/products")]
[EnableRateLimiting("api")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(new[]
        {
            new { Id = 1, Name = "Keyboard" },
            new { Id = 2, Name = "Mouse" }
        });
    }
}

Requests to this controller now use the api rate-limiting policy.

You can also apply a policy to a specific endpoint instead of an entire controller. For example, a product listing endpoint might allow a higher request rate than an expensive report-generation endpoint.

For minimal APIs, apply the policy with RequireRateLimiting:

app.MapGet("/api/products", () =>
{
    return Results.Ok(new[]
    {
        new { Id = 1, Name = "Keyboard" },
        new { Id = 2, Name = "Mouse" }
    });
})
.RequireRateLimiting("api");

Use endpoint-level policies when different parts of the API have different performance or business requirements.

Choose the Right Rate-Limiting Algorithm
Fixed-window limiting

A fixed-window limiter counts requests within a fixed period. Once the window expires, the counter resets.
options.AddFixedWindowLimiter("fixed", limiter =>
{
    limiter.PermitLimit = 60;
    limiter.Window = TimeSpan.FromMinutes(1);
    limiter.QueueLimit = 0;
    limiter.AutoReplenishment = true;
});

This configuration allows 60 requests per minute per partition.

Fixed windows are straightforward to configure and understand. However, they can allow bursts around window boundaries. A client might send 60 requests immediately before a window ends and another 60 just after the next window begins.

For workloads where boundary bursts are acceptable, a fixed window can be sufficient.

Sliding-window limiting

A sliding-window limiter divides a larger time window into smaller segments. When a new segment begins, older request counts gradually stop contributing to the active window.

options.AddSlidingWindowLimiter("sliding", limiter =>
{
    limiter.PermitLimit = 60;
    limiter.Window = TimeSpan.FromMinutes(1);
    limiter.SegmentsPerWindow = 6;
    limiter.QueueLimit = 0;
    limiter.AutoReplenishment = true;
});


This policy uses six segments in a one-minute window. It smooths the transition between windows compared with a fixed-window limiter.

Sliding windows are useful when you want to limit request frequency without allowing large bursts at window boundaries. They do not guarantee perfectly even spacing between every request.

Token-bucket limiting

A token bucket starts with a configured number of tokens and replenishes tokens over time. Each accepted request consumes a token.
options.AddTokenBucketLimiter("token-bucket", limiter =>
{
    limiter.TokenLimit = 20;
    limiter.TokensPerPeriod = 10;
    limiter.ReplenishmentPeriod =
        TimeSpan.FromSeconds(30);
    limiter.QueueLimit = 0;
    limiter.AutoReplenishment = true;
});


This configuration permits a burst of up to 20 requests when the bucket is full, then replenishes 10 tokens every 30 seconds.

Token buckets are useful when an API should tolerate short bursts while controlling sustained traffic. The precise behavior depends on the token capacity, replenishment rate, and current token count.

Concurrency limiting
A concurrency limiter restricts how many requests can execute at the same time.
options.AddConcurrencyLimiter("expensive", limiter =>
{
    limiter.PermitLimit = 10;
    limiter.QueueLimit = 0;
});


This policy allows up to 10 concurrent requests per partition. It does not limit the total number of requests a client can make over a minute if those requests complete quickly.

Concurrency limiting is useful for expensive operations such as report generation, large exports, or resource-intensive queries. It can protect a service from having too many costly operations running simultaneously.

Partition Requests by Client

The earlier examples use a named policy, but a limiter's partitioning strategy determines which requests share the same limit.

A global limiter can apply a common limit to all requests. A partitioned limiter can maintain separate limits for different users, API keys, tenants, or other client identifiers.

For example, the following policy uses an authenticated user's identity when available and otherwise falls back to the remote IP address:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode =
        StatusCodes.Status429TooManyRequests;

    options.AddPolicy("per-client", context =>
    {
        var userId = context.User.Identity?.IsAuthenticated == true
            ? context.User.Identity.Name
            : null;

        var clientKey = !string.IsNullOrWhiteSpace(userId)
            ? $"user:{userId}"
            : $"ip:{context.Connection.RemoteIpAddress}";

        return RateLimitPartition.GetFixedWindowLimiter(
            clientKey,
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 60,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0,
                AutoReplenishment = true
            });
    });
});

Apply this policy to the required endpoints:
app.MapGet("/api/orders", () =>
{
    return Results.Ok("Order list");
})
.RequireRateLimiting("per-client");


This is a starting point rather than a complete identity strategy. Identity.Name may be null or unsuitable for partitioning if the authentication configuration does not provide a stable unique value. In a production application, use a trusted, stable user identifier or a validated API-key identifier.

The partition key should also be normalized and bounded. Creating partitions from arbitrary attacker-controlled strings can consume memory as the number of distinct keys grows.

For anonymous clients, IP-based partitioning is only as reliable as the remote address presented to the application. When the application is behind a reverse proxy or load balancer, configure forwarded-header processing and trust only known proxies. Do not trust arbitrary client-supplied forwarding headers, because an attacker could manipulate the apparent IP address to bypass a limit.

Return a Useful HTTP 429 Response

When a client exceeds a rate limit, HTTP 429 Too Many Requests is the standard status code for communicating that the request was rejected because of rate limiting.

ASP.NET Core lets you customize the rejection response:
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode =
        StatusCodes.Status429TooManyRequests;

    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.ContentType =
            "application/json";

        await context.HttpContext.Response.WriteAsJsonAsync(
            new
            {
                error = "rate_limit_exceeded",
                message =
                    "Too many requests. Please try again later."
            },
            cancellationToken: token);
    };
});

This produces a JSON response when the limiter rejects a request. The application can also include a suitable Retry-After header when it can calculate a reliable retry time for the selected policy. Do not promise a precise retry time unless the application can determine it accurately. For a token bucket or a partitioned policy, the correct retry interval may depend on the current state of the limiter.

Clients should treat HTTP 429 as a signal to slow down. They should avoid immediately retrying every rejected request, which can increase pressure on the service.

Understand In-Memory Limits in Multi-Instance Deployments
The built-in ASP.NET Core rate limiter maintains its limiter state in the application process. It is not automatically a shared, distributed rate limiter.
Suppose an application runs on three server instances behind a load balancer. Each instance has its own in-memory counter. If each instance allows 100 requests per minute for a given partition, a client distributed across all three instances may be able to make substantially more than 100 requests during that period.

This behavior is important when enforcing account quotas, paid API plans, or security-related limits.

For simple per-instance protection, the built-in limiter may be sufficient. For a strict limit shared across multiple instances, use a distributed rate-limiting solution or enforce the quota at a gateway or service that maintains shared state.

An API gateway can also provide an additional layer of protection before requests reach the application. However, gateway limits and application-level limits solve different problems and may be used together.

Configure Rate Limiting Alongside Authentication
A rate limiter should use an identity that matches the intended policy.

For public endpoints, limiting by IP address may be useful. For authenticated APIs, limiting by user or tenant can provide fairer quotas. For API-key-based services, a validated key identifier can be more appropriate than the source IP.

If a policy depends on HttpContext.User, ensure that authentication runs before rate limiting evaluates the endpoint policy.

A typical middleware sequence for a controller application is:
app.UseRouting();

app.UseAuthentication();
app.UseRateLimiter();
app.UseAuthorization();

app.MapControllers();


The exact ordering should match the application's routing, authentication, and authorization requirements. If authentication endpoints themselves need protection, consider a separate policy that can also protect anonymous requests.

Rate limiting does not replace authorization. A client that remains under its rate limit must still be prevented from accessing resources it is not permitted to use.

Test Rate-Limiting Behavior

Do not validate a rate limiter only by inspecting its configuration. Send enough requests to confirm that it accepts requests within the limit and rejects excess requests.

For a policy that allows five requests, a test can send requests sequentially and inspect their status codes. Keep the test isolated from other requests sharing the same partition, and account for the configured time window.

Verify that:

  • Requests within the limit succeed.
  • Requests beyond the limit receive HTTP 429.
  • The rejection response has the expected format.
  • Different users or tenants receive separate limits when intended.
  • Anonymous requests cannot bypass the policy by manipulating untrusted headers.
  • Cancellation and timeouts behave appropriately for queued requests.
  • The effective limit is correct when the application runs on multiple instances.

If the application uses a queue, test the queue behavior as well. Queuing can smooth short bursts, but it can also increase latency and consume resources. For APIs that should reject excess traffic immediately, a queue limit of zero is often the simpler choice.

Common Mistakes to Avoid

Several implementation mistakes can make rate limiting ineffective or unnecessarily disruptive:

  • Defining a policy but never attaching it to an endpoint.
  • Using one limit for every endpoint despite significant differences in cost.
  • Treating a concurrency limit as a requests-per-minute quota.
  • Trusting arbitrary forwarding headers for client IP addresses.
  • Using unstable or attacker-controlled partition keys.
  • Assuming in-memory limits are shared across application instances.
  • Returning HTTP 500 instead of HTTP 429 when a request is rate-limited.
  • Allowing clients to retry rejected requests immediately.
  • Relying on rate limiting instead of authentication and authorization.
  • Applying limits without testing legitimate burst traffic and expected usage.

The best configuration is one that protects the service without unnecessarily rejecting normal client activity.

Conclusion

Rate-limiting middleware for fixed-window, sliding-window, token-bucket, and concurrency-based rules is included into ASP.NET Core. These systems aid in regulating the frequency of requests and shielding application resources from overuse.

Apply the policy at the proper controller or route after first selecting one that corresponds to the endpoint's workload. Partition limitations using trusted client identities, test the policy in practical scenarios, and return HTTP 429 for denied requests.


For applications running across multiple instances, remember that the built-in limiter is process-local. If you need a strict shared quota, use an appropriate distributed or gateway-level solution.

Rate limiting works best as one part of an API protection strategy that also includes authentication, authorization, monitoring, and deliberate capacity planning.

HostForLIFE.eu ASP.NET Core 11.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.