Rate limiting (a.k.a API throttling) protects your APIs from abuse, prevents overload, and enforces fair usage. In ASP.NET Core you can implement it at many levels: per-IP, per-user, per-endpoint, and globally — using built-in middleware, third-party libraries, or a distributed store (Redis) for multi-instance scenarios.
This guide explains practical strategies, shows ready-to-drop-in code (modern ASP.NET Core), and covers production concerns: headers, retry hints, metrics, and pattern recommendations.
1. Goals & common policies
Before implementation, decide what you want to protect against. Typical goals:
Prevent too many requests from a single client (DoS protection).
Enforce fair API quotas for paid/free tiers (business rules).
Smooth bursts while allowing sustained throughput (token bucket).
Rate limit across multiple app instances (distributed counters).
Common rate limit strategies
Fixed window: count requests per fixed interval (easy, bursty).
Sliding window: smoother than fixed window (slightly more complex).
Leaky bucket / Token bucket: allow bursts up to capacity, then steady rate.
Concurrency limit: limit concurrent active operations (server load control).
Request cost: weight endpoints by cost (heavy endpoints use more tokens).
2. Built-in Rate Limiting (ASP.NET Core 7+)
.NET provides Microsoft.AspNetCore.RateLimiting (and System.Threading.RateLimiting) with RateLimiter, TokenBucketRateLimiter, and PartitionedRateLimiter. It integrates as middleware and is the recommended starting point for single-instance apps or when using a distributed custom partitioner.
2.1 Simple Token Bucket per IP (example)
// Program.cs (minimal API / .NET 7+)
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// Register rate limiting
builder.Services.AddRateLimiter(options =>
{
// Partition by remote IP
options.AddPolicy("PerIpPolicy", context =>
RateLimitPartition.GetRemoteIpAddressPolicy(context, ip =>
new TokenBucketRateLimiterOptions
{
// allow 10 tokens initially, refill 1 token per second, capacity 10
TokenLimit = 10,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 1
}));
});
var app = builder.Build();
// Use middleware
app.UseRateLimiter();
// Example endpoint with policy
app.MapGet("/api/data", () => Results.Ok("OK"))
.RequireRateLimiting("PerIpPolicy");
app.Run();
// Helper extension
public static class RateLimitPartition
{
public static Func<HttpContext, string> GetRemoteIpAddressPolicy = context =>
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return ip;
};
public static PartitionedRateLimiter<HttpContext, string> GetRemoteIpAddressPolicy(
HttpContext context, Func<string, TokenBucketRateLimiterOptions> optionsFactory) =>
PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetRemoteIpAddressPolicy(context) is string key
? RateLimitPartition.GetPartitionForKey(key, optionsFactory(key))
: RateLimitPartition.GetPartitionForKey("unknown", optionsFactory("unknown")));
// ... implement GetPartitionForKey if needed or use built-in helper above in AddPolicy
}
Notes
This built-in approach uses in-memory token buckets (per app instance).
TokenLimitis burst capacity;TokensPerPeriodcontrols steady rate.
2.2 Per-User or Per-API Key Partitioning
If your API authenticates clients, partition by user id or API key instead of IP:
options.AddPolicy("PerUserPolicy", context =>
{
var userId = context.User?.FindFirst("sub")?.Value ?? "anon";
return RateLimitPartition.GetFixedWindowLimiter(userId, _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
});
});
3. Distributed Rate Limiting (multi-instance) — Redis approach
In multi-instance scenarios (Kubernetes, multiple app servers), in-memory counters don't work. Use a distributed store such as Redis.
Two practical approaches:
Use a library that implements distributed rate limiting (e.g., [AspNetCoreRateLimit] is popular but mainly in-memory; other libraries exist).
Implement a simple Redis-backed fixed-window or token-bucket using atomic Redis commands (INCR + EXPIRE or Lua script) and custom middleware.
3.1 Redis fixed-window example (middleware)
Pros: simple, works well for quotas.
Cons: fixed-window allows all burst at boundary; sliding window or leaky bucket with Lua is better for smoothing.
// RedisRateLimitMiddleware.cs
using StackExchange.Redis;
using Microsoft.AspNetCore.Http;
using System.Text.Json;
public class RedisRateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly IDatabase _redis;
private readonly int _limit;
private readonly TimeSpan _window;
public RedisRateLimitMiddleware(RequestDelegate next, IConnectionMultiplexer multiplexer, int limit = 100, TimeSpan? window = null)
{
_next = next;
_redis = multiplexer.GetDatabase();
_limit = limit;
_window = window ?? TimeSpan.FromMinutes(1);
}
public async Task InvokeAsync(HttpContext context)
{
var key = GetKey(context); // e.g., $"{context.Request.Path}:{clientId}"
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var windowKey = $"{key}:{now / (long)_window.TotalSeconds}"; // fixed window bucket
// Atomic increment
var count = (int)await _redis.StringIncrementAsync(windowKey);
if (count == 1)
{
// set expiry for the bucket
await _redis.KeyExpireAsync(windowKey, _window);
}
if (count > _limit)
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.Response.Headers["Retry-After"] = _window.TotalSeconds.ToString();
await context.Response.WriteAsync("Rate limit exceeded");
return;
}
// Optionally set headers with remaining
context.Response.Headers["X-RateLimit-Limit"] = _limit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = Math.Max(0, _limit - count).ToString();
await _next(context);
}
private static string GetKey(HttpContext context)
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return $"rl:{ip}:{context.Request.Path}";
}
}

Join the conversation! Your thoughts help the community grow.