← Back to Articles         
Types of API Rate Limiting

Types of API Rate Limiting

Published on 23 Sep 2026     13 min read Web API
Rate Limiting

Types of Rate Limiting in ASP.NET Core Web API

Rate limiting controls how many requests an API accepts from a client within a given period or how many requests it processes simultaneously.

It helps to:

  • Prevent API abuse.

  • Protect the server from overload.

  • Ensure fair usage among clients.

  • Control CPU, memory and database usage.

  • Reduce automated attacks.

  • Manage API usage based on subscription plans.

ASP.NET Core provides built-in rate-limiting middleware with four main algorithms:

  1. Fixed Window

  2. Sliding Window

  3. Token Bucket

  4. Concurrency

These are available through the Microsoft.AspNetCore.RateLimiting namespace. Microsoft documentation


Required namespaces

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

Usually, no additional NuGet package is required in modern ASP.NET Core Web API projects because the middleware is included in the ASP.NET Core shared framework.


1. Fixed Window Rate Limiting

What is Fixed Window?

Fixed Window divides time into fixed intervals and allows a specified number of requests during each interval.

For example:

Permit limit: 5 requests
Window:       10 seconds

The client can send a maximum of five requests during every fixed 10-second window.

Window 1: 00–10 seconds → 5 requests allowed
Window 2: 10–20 seconds → limit resets to 5
Window 3: 20–30 seconds → limit resets to 5

Program.cs configuration

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("FixedPolicy", policy =>
    {
        policy.PermitLimit = 5;
        policy.Window = TimeSpan.FromSeconds(10);

        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;

        policy.QueueLimit = 2;
    });
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRateLimiter();

app.MapControllers();

app.Run();

Apply it to a controller

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

[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("FixedPolicy")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(new[]
        {
            "Laptop",
            "Mobile",
            "Keyboard"
        });
    }
}

How it works

With the preceding configuration:

  • First 5 requests are accepted.

  • Next 2 requests can wait in the queue.

  • Further requests are rejected.

  • The request limit resets after 10 seconds.

Main disadvantage: boundary burst

Suppose the client sends:

  • 5 requests at the end of the first window.

  • 5 more requests at the beginning of the next window.

The server may receive 10 requests within a very short period.

Window 1                    Window 2
----------|-----------------|----------
        5 requests       5 requests

This boundary burst is the main weakness of Fixed Window.

Best use cases

Use Fixed Window when:

  • The rules are simple.

  • Small traffic bursts are acceptable.

  • You need limits such as “100 requests per minute.”

  • Exact smoothness is not important.


2. Sliding Window Rate Limiting

What is Sliding Window?

Sliding Window is similar to Fixed Window, but it divides each window into smaller segments.

As time moves forward, the oldest segment expires and its request capacity becomes available again.

For example:

Window:              60 seconds
Segments per window: 6
Permit limit:        100

Each segment represents:

60 ÷ 6 = 10 seconds

The limiter examines activity over the most recent 60 seconds, with capacity being recycled as each 10-second segment expires.

Program.cs configuration

builder.Services.AddRateLimiter(options =>
{
    options.AddSlidingWindowLimiter("SlidingPolicy", policy =>
    {
        policy.PermitLimit = 10;

        policy.Window = TimeSpan.FromSeconds(30);

        policy.SegmentsPerWindow = 3;

        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;

        policy.QueueLimit = 2;
    });
});

Apply it to an endpoint

[EnableRateLimiting("SlidingPolicy")]
[HttpGet("reports")]
public IActionResult GetReports()
{
    return Ok("Reports returned successfully.");
}

How it works

The configuration contains:

Window:              30 seconds
Segments:            3
Each segment:        10 seconds
Maximum requests:    10

Every 10 seconds, the oldest segment falls outside the active window. The requests recorded in that expired segment no longer count against the active limit.

This produces smoother traffic control than Fixed Window.

Advantages

  • Reduces the boundary-burst problem.

  • Produces smoother request distribution.

  • More accurate than Fixed Window.

  • Good for public APIs with continuous traffic.

Disadvantages

  • Slightly more complex.

  • Requires more tracking than Fixed Window.

  • Choosing too many segments can add overhead.

Best use cases

Use Sliding Window when:

  • Traffic must be controlled more smoothly.

  • Fixed-window boundary bursts are undesirable.

  • You have public APIs with continuous client traffic.

  • You need better fairness between users.


3. Token Bucket Rate Limiting

What is Token Bucket?

Token Bucket uses a virtual bucket containing tokens.

Each incoming request must consume a token:

1 request = 1 token

Tokens are added to the bucket periodically. If no tokens are available, the request is queued or rejected.

Simple scenario

Suppose:

Token limit:               10
Tokens added each period:   2
Replenishment period:      10 seconds

Initially, the bucket has 10 tokens.

Request arrives → consumes 1 token
Another request → consumes 1 token
10 requests     → all tokens used

After 10 seconds, two new tokens are added.

Therefore, another two requests can be processed.

Program.cs configuration

builder.Services.AddRateLimiter(options =>
{
    options.AddTokenBucketLimiter("TokenPolicy", policy =>
    {
        policy.TokenLimit = 10;

        policy.TokensPerPeriod = 2;

        policy.ReplenishmentPeriod =
            TimeSpan.FromSeconds(10);

        policy.AutoReplenishment = true;

        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;

        policy.QueueLimit = 2;
    });
});

Apply it to an endpoint

[EnableRateLimiting("TokenPolicy")]
[HttpPost("orders")]
public IActionResult CreateOrder()
{
    return Ok("Order created successfully.");
}

Important properties

TokenLimit

Maximum number of tokens the bucket can hold.

policy.TokenLimit = 10;

The bucket cannot contain more than 10 tokens.

TokensPerPeriod

Number of tokens added during each replenishment period.

policy.TokensPerPeriod = 2;

ReplenishmentPeriod

How frequently tokens are added.

policy.ReplenishmentPeriod =
    TimeSpan.FromSeconds(10);

AutoReplenishment

Automatically adds tokens at the configured interval.

policy.AutoReplenishment = true;

Advantages

  • Supports short bursts of traffic.

  • Controls the long-term average request rate.

  • Unused tokens can accumulate up to TokenLimit.

  • More flexible than window-based algorithms.

Disadvantages

  • Configuration requires careful planning.

  • Large bucket sizes can allow heavy bursts.

  • Small replenishment rates can unnecessarily slow clients.

Best use cases

Use Token Bucket when:

  • Short bursts should be allowed.

  • Long-term traffic must remain controlled.

  • An API has subscription plans.

  • Clients occasionally send batches of requests.

Example:

Free plan       → 10-token bucket
Standard plan   → 100-token bucket
Premium plan    → 1,000-token bucket

4. Concurrency Rate Limiting

What is Concurrency Limiting?

Concurrency Limiting controls how many requests can be processed at the same time.

It does not limit the number of requests per minute or second.

For example:

Permit limit: 3 concurrent requests
Queue limit:  2 requests

At any moment:

  • 3 requests can be processed.

  • 2 additional requests can wait.

  • Any further request is rejected.

When a running request completes, one queued request is allowed to start.

Program.cs configuration

builder.Services.AddRateLimiter(options =>
{
    options.AddConcurrencyLimiter("ConcurrencyPolicy", policy =>
    {
        policy.PermitLimit = 3;

        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;

        policy.QueueLimit = 2;
    });
});

Notice that Concurrency Limiter does not have a time window.

Apply it to an endpoint

[EnableRateLimiting("ConcurrencyPolicy")]
[HttpGet("generate-report")]
public async Task<IActionResult> GenerateReport()
{
    await Task.Delay(5000);

    return Ok("Report generated successfully.");
}

If three report-generation requests are already running:

  • The next two wait in the queue.

  • Requests beyond the queue capacity are rejected.

Advantages

  • Protects CPU-intensive operations.

  • Protects database connections.

  • Controls expensive report-generation processes.

  • Prevents too many long-running operations.

  • Useful when duration matters more than request count.

Important difference

Suppose each request completes in 10 milliseconds.

A Concurrency Limiter could process thousands of requests per minute while still allowing only three at the same moment.

Therefore:

Concurrency limiting controls simultaneous requests, not requests per time period.

Best use cases

Use Concurrency Limiter for:

  • Report generation.

  • File uploads.

  • Image or video processing.

  • Expensive database queries.

  • External API calls.

  • Long-running operations.

  • Limited server resources.


Comparison of the four algorithms

Type Controls Supports bursts? Has time window? Best use
Fixed Window Requests per fixed period Yes Yes Simple limits
Sliding Window Requests over a moving period Limited Yes Smooth and fair traffic
Token Bucket Requests based on available tokens Yes Replenishment period Controlled average with bursts
Concurrency Requests running simultaneously Not time-based No Expensive operations

Complete example with all four policies

Program.cs

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddRateLimiter(options =>
{
    // Default rejection status
    options.RejectionStatusCode =
        StatusCodes.Status429TooManyRequests;

    // 1. Fixed Window
    options.AddFixedWindowLimiter("FixedPolicy", policy =>
    {
        policy.PermitLimit = 5;
        policy.Window = TimeSpan.FromSeconds(10);
        policy.QueueLimit = 0;
        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;
    });

    // 2. Sliding Window
    options.AddSlidingWindowLimiter("SlidingPolicy", policy =>
    {
        policy.PermitLimit = 10;
        policy.Window = TimeSpan.FromSeconds(30);
        policy.SegmentsPerWindow = 3;
        policy.QueueLimit = 2;
        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;
    });

    // 3. Token Bucket
    options.AddTokenBucketLimiter("TokenPolicy", policy =>
    {
        policy.TokenLimit = 10;
        policy.TokensPerPeriod = 2;
        policy.ReplenishmentPeriod =
            TimeSpan.FromSeconds(10);
        policy.AutoReplenishment = true;
        policy.QueueLimit = 2;
        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;
    });

    // 4. Concurrency
    options.AddConcurrencyLimiter(
        "ConcurrencyPolicy", policy =>
    {
        policy.PermitLimit = 3;
        policy.QueueLimit = 2;
        policy.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;
    });

    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode =
            StatusCodes.Status429TooManyRequests;

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

var app = builder.Build();

app.UseHttpsRedirection();

app.UseRateLimiter();

app.MapControllers();

app.Run();

Applying different policies to different endpoints

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

[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [EnableRateLimiting("FixedPolicy")]
    [HttpGet("products")]
    public IActionResult GetProducts()
    {
        return Ok("Fixed Window applied.");
    }

    [EnableRateLimiting("SlidingPolicy")]
    [HttpGet("search")]
    public IActionResult Search()
    {
        return Ok("Sliding Window applied.");
    }

    [EnableRateLimiting("TokenPolicy")]
    [HttpPost("orders")]
    public IActionResult CreateOrder()
    {
        return Ok("Token Bucket applied.");
    }

    [EnableRateLimiting("ConcurrencyPolicy")]
    [HttpGet("reports")]
    public async Task<IActionResult> GenerateReport()
    {
        await Task.Delay(3000);

        return Ok("Concurrency Limiter applied.");
    }
}

Important configuration properties

PermitLimit

Maximum permits available.

For window algorithms, this generally means requests per window:

policy.PermitLimit = 10;

For Concurrency Limiter, it means the maximum number of simultaneous requests.


QueueLimit

Maximum number of requests allowed to wait.

policy.QueueLimit = 2;

Use zero to disable queuing:

policy.QueueLimit = 0;

Large queues are generally dangerous because they can:

  • Increase response time.

  • Consume memory.

  • Cause request timeouts.

  • Hide an overloaded server.


QueueProcessingOrder

Determines which queued request is processed first.

Oldest first

policy.QueueProcessingOrder =
    QueueProcessingOrder.OldestFirst;

Works like FIFO:

Oldest request → processed first

This normally provides better fairness.

Newest first

policy.QueueProcessingOrder =
    QueueProcessingOrder.NewestFirst;

Newer requests receive priority. When the queue is full, older queued requests may be dropped to make room for newer requests.

Use this only when fresh requests are more valuable than stale ones.


Returning 429 Too Many Requests

When a request exceeds the limit, the API normally returns:

HTTP/1.1 429 Too Many Requests

Configure it explicitly:

options.RejectionStatusCode =
    StatusCodes.Status429TooManyRequests;

Custom response:

options.OnRejected = async (context, token) =>
{
    context.HttpContext.Response.StatusCode =
        StatusCodes.Status429TooManyRequests;

    await context.HttpContext.Response.WriteAsJsonAsync(
        new
        {
            statusCode = 429,
            message = "Rate limit exceeded.",
            suggestion = "Please try again later."
        },
        cancellationToken: token);
};

Adding the Retry-After header

Some limiters can provide information indicating how long the client should wait.

options.OnRejected = async (context, token) =>
{
    if (context.Lease.TryGetMetadata(
        MetadataName.RetryAfter,
        out TimeSpan retryAfter))
    {
        context.HttpContext.Response.Headers.RetryAfter =
            ((int)retryAfter.TotalSeconds).ToString();
    }

    context.HttpContext.Response.StatusCode =
        StatusCodes.Status429TooManyRequests;

    await context.HttpContext.Response.WriteAsJsonAsync(
        new
        {
            statusCode = 429,
            message = "Rate limit exceeded.",
            retryAfterSeconds =
                context.Lease.TryGetMetadata(
                    MetadataName.RetryAfter,
                    out TimeSpan delay)
                    ? (int?)delay.TotalSeconds
                    : null
        },
        cancellationToken: token);
};

Possible response:

HTTP/1.1 429 Too Many Requests
Retry-After: 8
Content-Type: application/json
{
  "statusCode": 429,
  "message": "Rate limit exceeded.",
  "retryAfterSeconds": 8
}

The client should wait before retrying.


Global rate limiting

A global limiter applies automatically to all endpoints.

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter =
        PartitionedRateLimiter.Create<HttpContext, string>(
            httpContext =>
                RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey: "Global",
                    factory: _ =>
                        new FixedWindowRateLimiterOptions
                        {
                            PermitLimit = 100,
                            Window =
                                TimeSpan.FromMinutes(1),
                            QueueLimit = 0,
                            AutoReplenishment = true
                        }));
});

This shares one limit across every request and every client, which is rarely ideal for a public production API.

Usually, partitioning by user, API key, tenant or IP address is better.


Partitioned rate limiting

What is partitioning?

Partitioning creates a separate rate limit for each client or category.

Without partitioning:

All clients share one limit

With partitioning:

User A → separate limit
User B → separate limit
User C → separate limit

Limit by authenticated username

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter =
        PartitionedRateLimiter.Create<HttpContext, string>(
            httpContext =>
            {
                string userName =
                    httpContext.User.Identity?.Name
                    ?? "Anonymous";

                return RateLimitPartition
                    .GetFixedWindowLimiter(
                        partitionKey: userName,
                        factory: _ =>
                            new FixedWindowRateLimiterOptions
                            {
                                PermitLimit = 10,
                                Window =
                                    TimeSpan.FromMinutes(1),
                                QueueLimit = 0,
                                AutoReplenishment = true
                            });
            });
});

Each user receives ten requests per minute.

Limit by IP address

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter =
        PartitionedRateLimiter.Create<HttpContext, string>(
            httpContext =>
            {
                string ipAddress =
                    httpContext.Connection
                               .RemoteIpAddress?
                               .ToString()
                    ?? "Unknown";

                return RateLimitPartition
                    .GetFixedWindowLimiter(
                        partitionKey: ipAddress,
                        factory: _ =>
                            new FixedWindowRateLimiterOptions
                            {
                                PermitLimit = 20,
                                Window =
                                    TimeSpan.FromMinutes(1),
                                QueueLimit = 0,
                                AutoReplenishment = true
                            });
            });
});

Every IP address receives its own limit.

Be careful when the application is behind a proxy or load balancer. Correct forwarded-header configuration is necessary before relying on RemoteIpAddress.

Common partition keys

You can partition by:

  • Authenticated user ID.

  • JWT claim.

  • Tenant ID.

  • API key.

  • Subscription level.

  • IP address.

  • Request host.

  • Endpoint category.


Chained rate limiting

Multiple limiters can be combined so a request must satisfy every limiter.

Example requirement:

Per-client rule:  10 requests per minute
Global rule:      Maximum 50 concurrent requests

This protects the API against:

  • A single abusive client.

  • Total application overload.

Conceptually:

Request
   ↓
Per-user rate limit
   ↓
Global concurrency limit
   ↓
API endpoint

Chaining is useful in production because one algorithm rarely addresses every risk.


Named policy vs global policy

Named policy

A named policy is applied only to selected endpoints:

options.AddFixedWindowLimiter("FixedPolicy", ...);

Apply with:

[EnableRateLimiting("FixedPolicy")]

or:

app.MapControllers()
   .RequireRateLimiting("FixedPolicy");

Global policy

A global policy automatically applies to all endpoints:

options.GlobalLimiter = ...;

If an endpoint has both a global limiter and an endpoint-specific limiter, both can affect the request.


Disable rate limiting for an endpoint

[DisableRateLimiting]
[HttpGet("health")]
public IActionResult Health()
{
    return Ok("Healthy");
}

This can be useful for carefully chosen endpoints such as internal health checks. However, disabling endpoint-level limiting does not necessarily bypass a configured global limiter.


Which limiter should you choose?

Use Fixed Window when

  • Requirements are simple.

  • A small burst near the window boundary is acceptable.

  • You want easy configuration.

Example:

100 requests per minute

Use Sliding Window when

  • You need smoother traffic.

  • Fairness is important.

  • Boundary bursts should be reduced.

Example:

Public search API

Use Token Bucket when

  • Short bursts are acceptable.

  • Long-term average traffic must be controlled.

  • Clients sometimes perform batch operations.

Example:

An API allows 100 immediate requests and restores
10 tokens every minute.

Use Concurrency when

  • Operations are slow or resource-intensive.

  • You must protect database or CPU capacity.

  • The request count matters less than simultaneous execution.

Example:

Only five reports may be generated simultaneously.

Production recommendations

  • Prefer per-user, per-tenant or per-API-key partitioning.

  • Avoid relying only on a shared global limit.

  • Return 429 Too Many Requests for rate rejection.

  • Add a Retry-After header when available.

  • Keep queues small or disable them.

  • Use Concurrency Limiting for expensive endpoints.

  • Consider combining a time-based limiter with a Concurrency Limiter.

  • Store limits in configuration instead of hard-coding them.

  • Load-test limits before deploying.

  • Monitor rejected requests and queue duration.

  • Configure forwarded headers properly before limiting by IP.

  • For multiple application instances, remember that the built-in limiter normally operates independently inside each server process.

  • Use an API gateway or distributed rate-limiting solution when a single limit must be enforced across multiple servers.

  • Rate limiting helps mitigate abuse, but it does not replace WAF or DDoS protection.

Key points

  • ASP.NET Core provides four built-in rate-limiting algorithms.

  • Fixed Window is simple but can create boundary bursts.

  • Sliding Window produces smoother and fairer traffic control.

  • Token Bucket allows controlled bursts and gradual replenishment.

  • Concurrency Limiter controls simultaneous requests, not requests per minute.

  • AddRateLimiter() registers rate-limiting services.

  • UseRateLimiter() enables the middleware.

  • [EnableRateLimiting] applies a named policy.

  • [DisableRateLimiting] disables endpoint-specific rate limiting.

  • Rejected requests normally receive 429 Too Many Requests.

  • Partitioning provides separate limits for users, tenants, API keys or IP addresses.

  • Chained policies can combine client-level and server-level protection.

  • Rate limiting supports security but does not replace dedicated DDoS protection.

← Back to Articles