Introduction

When building production-grade APIs in ASP.NET Core, uncontrolled traffic is one of the most common causes of performance degradation and security exposure. High-frequency requests—whether from legitimate spikes, poorly implemented clients, or malicious bots—can overwhelm your application.

API rate limiting introduces a controlled access policy: a client is allowed a fixed number of requests within a defined time window. Once the threshold is exceeded, further requests are temporarily rejected.

This guide demonstrates how to implement API rate limiting using custom middleware in ASP.NET Core, along with practical reasoning, real-world scenarios, and production considerations.

What is API Rate Limiting?

API rate limiting is a mechanism that restricts the number of API calls a client can make over a specified duration. It is a foundational aspect of ASP.NET Core API security and performance optimization.

Real-world analogy

Consider a customer support line that allows only a limited number of calls per minute per user. This ensures fair access and prevents system overload.

Why it matters

How API Rate Limiting Works Internally

At a system level, rate limiting tracks:

Based on these values, the system decides whether to:

Step 1: Create a Rate Limit Options Class

public class RateLimitOptions
{
    public int MaxRequests { get; set; } = 5;
    public TimeSpan TimeWindow { get; set; } = TimeSpan.FromMinutes(1);
}

Explanation

This configuration class defines the rate limiting policy:

This abstraction allows easy configuration changes without modifying middleware logic.

Step 2: Create a Request Tracking Model

public class ClientRequestInfo
{
    public int RequestCount { get; set; }
    public DateTime WindowStart { get; set; }
}

Explanation

This model stores request metadata per client:

This enables accurate reset logic once the time window expires.

Step 3: Implement the Rate Limiting Middleware

using System.Collections.Concurrent;

public class RateLimitingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RateLimitOptions _options;
    private static readonly ConcurrentDictionary<string, ClientRequestInfo> _clients = new();

    public RateLimitingMiddleware(RequestDelegate next, RateLimitOptions options)
    {
        _next = next;
        _options = options;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var clientIp = context.Connection.RemoteIpAddress?.ToString();

        if (string.IsNullOrEmpty(clientIp))
        {
            await _next(context);
            return;
        }

        var client = _clients.GetOrAdd(clientIp, _ => new ClientRequestInfo
        {
            RequestCount = 0,
            WindowStart = DateTime.UtcNow
        });

        lock (client)
        {
            if (DateTime.UtcNow - client.WindowStart > _options.TimeWindow)
            {
                client.RequestCount = 0;
                client.WindowStart = DateTime.UtcNow;
            }

            client.RequestCount++;

            if (client.RequestCount > _options.MaxRequests)
            {
                context.Response.StatusCode = 429;
                context.Response.Headers["Retry-After"] = _options.TimeWindow.TotalSeconds.ToString();
                context.Response.WriteAsync("Too many requests. Try again later.");
                return;
            }
        }

        await _next(context);
    }
}

Explanation

This middleware executes on every request and applies the rate limiting logic:

Client Identification

Data Storage

Window Management

Enforcement

Thread Safety

Step 4: Register Middleware in Program.cs

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

var rateLimitOptions = new RateLimitOptions
{
    MaxRequests = 5,
    TimeWindow = TimeSpan.FromMinutes(1)
};

app.UseMiddleware<RateLimitingMiddleware>(rateLimitOptions);

app.MapControllers();

app.Run();

Explanation

Step 5: Testing the Implementation

Tools

Expected Behavior

This confirms that the ASP.NET Core rate limiting middleware is functioning correctly.

Before vs After Impact

Without Rate Limiting

With Rate Limiting

Common Pitfalls

Using Only IP Address

In-Memory Storage Limitation

Missing Retry Headers

Over-restricting Endpoints

Production Considerations

Recommended Enhancements

Built-in Rate Limiting in ASP.NET Core (.NET 7+)

builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 5;
        opt.Window = TimeSpan.FromMinutes(1);
    });
});

app.UseRateLimiter();

Explanation

Advantages

Limitations

Summary

API rate limiting in ASP.NET Core is essential for maintaining application stability, security, and fairness in high-traffic environments. By implementing a middleware-based solution, developers gain fine-grained control over request handling. While the custom approach is suitable for learning and smaller applications, production systems should adopt distributed strategies such as Redis-backed rate limiting or the built-in ASP.NET Core rate limiter for scalability and resilience.