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
Prevents API abuse and brute-force attacks
Maintains consistent application performance
Protects backend resources (CPU, memory, DB)
Ensures fair usage across clients
How API Rate Limiting Works Internally
At a system level, rate limiting tracks:
Client identity (IP address, API key, or user ID)
Number of requests made
Time window for those requests
Based on these values, the system decides whether to:
Allow the request
Reject the request with HTTP 429 (Too Many Requests)
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:
MaxRequests: Maximum number of allowed requests within the time windowTimeWindow: Duration in which those requests are counted
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:
RequestCount: Tracks how many requests have been madeWindowStart: Marks the beginning of the current time window
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);
}
}
Join the conversation! Your thoughts help the community grow.