Training
Module
Protect your APIs on Azure API Management - Training
Protect your backend APIs from information exposure and implement throttling (rate limiting) to prevent resource exhaustion with policies in Azure API Management.
This browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
By Arvin Kahbazi, Maarten Balliauw, and Rick Anderson
The Microsoft.AspNetCore.RateLimiting
middleware provides rate limiting middleware. Apps configure rate limiting policies and then attach the policies to endpoints. Apps using rate limiting should be carefully load tested and reviewed before deploying. See Testing endpoints with rate limiting in this article for more information.
For an introduction to rate limiting, see Rate limiting middleware.
The RateLimiterOptionsExtensions
class provides the following extension methods for rate limiting:
The AddFixedWindowLimiter
method uses a fixed time window to limit requests. When the time window expires, a new time window starts and the request limit is reset.
Consider the following code:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 4;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 2;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
.RequireRateLimiting("fixed");
app.Run();
The preceding code:
AddFixedWindowLimiter
to create a fixed window limiter with a policy name of "fixed"
and sets:Apps should use Configuration to set limiter options. The following code updates the preceding code using MyRateLimitOptions
for configuration:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Fixed Window Limiter {GetTicks()}"))
.RequireRateLimiting(fixedPolicy);
app.Run();
UseRateLimiter must be called after UseRouting
when rate limiting endpoint specific APIs are used. For example, if the [EnableRateLimiting]
attribute is used, UseRateLimiter
must be called after UseRouting
. When calling only global limiters, UseRateLimiter
can be called before UseRouting
.
A sliding window algorithm:
permitLimit
requests.n
segments per window.n
segments prior to the current segment) are added to the current segment. We refer to the most expired time segment one window back as the expired segment.Consider the following table that shows a sliding window limiter with a 30-second window, three segments per window, and a limit of 100 requests:
The following table shows the data in the previous graph in a different format. The Available column shows the requests available from the previous segment (The Carry over from the previous row). The first row shows 100 available requests because there's no previous segment.
Time | Available | Taken | Recycled from expired | Carry over |
---|---|---|---|---|
0 | 100 | 20 | 0 | 80 |
10 | 80 | 30 | 0 | 50 |
20 | 50 | 40 | 0 | 10 |
30 | 10 | 30 | 20 | 0 |
40 | 0 | 10 | 30 | 20 |
50 | 20 | 10 | 40 | 50 |
60 | 50 | 35 | 30 | 45 |
The following code uses the sliding window rate limiter:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Sliding Window Limiter {GetTicks()}"))
.RequireRateLimiting(slidingPolicy);
app.Run();
The token bucket limiter is similar to the sliding window limiter, but rather than adding back the requests taken from the expired segment, a fixed number of tokens are added each replenishment period. The tokens added each segment can't increase the available tokens to a number higher than the token bucket limit. The following table shows a token bucket limiter with a limit of 100 tokens and a 10-second replenishment period.
Time | Available | Taken | Added | Carry over |
---|---|---|---|---|
0 | 100 | 20 | 0 | 80 |
10 | 80 | 10 | 20 | 90 |
20 | 90 | 5 | 15 | 100 |
30 | 100 | 30 | 20 | 90 |
40 | 90 | 6 | 16 | 100 |
50 | 100 | 40 | 20 | 80 |
60 | 80 | 50 | 20 | 50 |
The following code uses the token bucket limiter:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var tokenPolicy = "token";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddTokenBucketLimiter(policyName: tokenPolicy, options =>
{
options.TokenLimit = myOptions.TokenLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
options.ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod);
options.TokensPerPeriod = myOptions.TokensPerPeriod;
options.AutoReplenishment = myOptions.AutoReplenishment;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
.RequireRateLimiting(tokenPolicy);
app.Run();
When AutoReplenishment is set to true
, an internal timer replenishes the tokens every ReplenishmentPeriod; when set to false
, the app must call TryReplenish on the limiter.
The concurrency limiter limits the number of concurrent requests. Each request reduces the concurrency limit by one. When a request completes, the limit is increased by one. Unlike the other requests limiters that limit the total number of requests for a specified period, the concurrency limiter limits only the number of concurrent requests and doesn't cap the number of requests in a time period.
The following code uses the concurrency limiter:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var concurrencyPolicy = "Concurrency";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: concurrencyPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", async () =>
{
await Task.Delay(500);
return Results.Ok($"Concurrency Limiter {GetTicks()}");
}).RequireRateLimiting(concurrencyPolicy);
app.Run();
The CreateChained API allows passing in multiple PartitionedRateLimiter which are combined into one PartitionedRateLimiter
. The combined limiter runs all the input limiters in sequence.
The following code uses CreateChained
:
using System.Globalization;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(_ =>
{
_.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", cancellationToken);
};
_.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 4,
Window = TimeSpan.FromSeconds(2)
});
}),
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetFixedWindowLimiter
(userAgent, _ =>
new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 20,
Window = TimeSpan.FromSeconds(30)
});
}));
});
var app = builder.Build();
app.UseRateLimiter();
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"));
app.Run();
For more information, see the CreateChained source code
The [EnableRateLimiting]
and [DisableRateLimiting]
attributes can be applied to a Controller, action method, or Razor Page. For Razor Pages, the attribute must be applied to the Razor Page and not the page handlers. For example, [EnableRateLimiting]
can't be applied to OnGet
, OnPost
, or any other page handler.
The [DisableRateLimiting]
attribute disables rate limiting to the Controller, action method, or Razor Page regardless of named rate limiters or global limiters applied. For example, consider the following code which calls RequireRateLimiting to apply the fixedPolicy
rate limiting to all controller endpoints:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages().RequireRateLimiting(slidingPolicy);
app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy);
app.Run();
In the following code, [DisableRateLimiting]
disables rate limiting and overrides [EnableRateLimiting("fixed")]
applied to the Home2Controller
and app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
called in Program.cs
:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
In the preceding code, the [EnableRateLimiting("sliding")]
is not applied to the Privacy
action method because Program.cs
called app.MapDefaultControllerRoute().RequireRateLimiting(fixedPolicy)
.
Consider the following code which doesn't call RequireRateLimiting
on MapRazorPages
or MapDefaultControllerRoute
:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var fixedPolicy = "fixed";
builder.Services.AddRateLimiter(_ => _
.AddFixedWindowLimiter(policyName: fixedPolicy, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var slidingPolicy = "sliding";
builder.Services.AddRateLimiter(_ => _
.AddSlidingWindowLimiter(policyName: slidingPolicy, options =>
{
options.PermitLimit = myOptions.SlidingPermitLimit;
options.Window = TimeSpan.FromSeconds(myOptions.Window);
options.SegmentsPerWindow = myOptions.SegmentsPerWindow;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
}));
var app = builder.Build();
app.UseRateLimiter();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapRazorPages();
app.MapDefaultControllerRoute(); // RequireRateLimiting not called
app.Run();
Consider the following controller:
[EnableRateLimiting("fixed")]
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;
public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}
public ActionResult Index()
{
return View();
}
[EnableRateLimiting("sliding")]
public ActionResult Privacy()
{
return View();
}
[DisableRateLimiting]
public ActionResult NoLimit()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
In the preceding controller:
"fixed"
policy rate limiter is applied to all action methods that don't have EnableRateLimiting
and DisableRateLimiting
attributes."sliding"
policy rate limiter is applied to the Privacy
action.NoLimit
action method.For Razor Pages, the attribute must be applied to the Razor Page and not the page handlers. For example, [EnableRateLimiting]
can't be applied to OnGet
, OnPost
, or any other page handler.
The DisableRateLimiting
attribute disables rate limiting on a Razor Page. EnableRateLimiting
is only applied to a Razor Page if MapRazorPages().RequireRateLimiting(Policy)
has not been called.
The fixed, sliding, and token limiters all limit the maximum number of requests in a time period. The concurrency limiter limits only the number of concurrent requests and doesn't cap the number of requests in a time period. The cost of an endpoint should be considered when selecting a limiter. The cost of an endpoint includes the resources used, for example, time, data access, CPU, and I/O.
The following samples aren't meant for production code but are examples on how to use the limiters.
The following sample:
Creates a RateLimiterOptions.OnRejected callback that is called when a request exceeds the specified limit. retryAfter
can be used with the TokenBucketRateLimiter, FixedWindowLimiter, and SlidingWindowLimiter because these algorithms are able to estimate when more permits will be added. The ConcurrencyLimiter
has no way of calculating when permits will be available.
Adds the following limiters:
SampleRateLimiterPolicy
which implements the IRateLimiterPolicy<TPartitionKey>
interface. The SampleRateLimiterPolicy
class is shown later in this article.SlidingWindowLimiter
:
GlobalLimiter
creates a partition for each IPAddress.using System.Globalization;
using System.Net;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRateLimitAuth;
using WebRateLimitAuth.Data;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.Configure<MyRateLimitOptions>(
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var userPolicyName = "user";
var helloPolicy = "hello";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.OnRejected = (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
.LogWarning("OnRejected: {GetUserEndPoint}", GetUserEndPoint(context.HttpContext));
return new ValueTask();
};
limiterOptions.AddPolicy<string, SampleRateLimiterPolicy>(helloPolicy);
limiterOptions.AddPolicy(userPolicyName, context =>
{
var username = "anonymous user";
if (context.User.Identity?.IsAuthenticated is true)
{
username = context.User.ToString()!;
}
return RateLimitPartition.GetSlidingWindowLimiter(username,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = myOptions.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
Window = TimeSpan.FromSeconds(myOptions.Window),
SegmentsPerWindow = myOptions.SegmentsPerWindow
});
});
limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, IPAddress>(context =>
{
IPAddress? remoteIpAddress = context.Connection.RemoteIpAddress;
if (!IPAddress.IsLoopback(remoteIpAddress!))
{
return RateLimitPartition.GetTokenBucketLimiter
(remoteIpAddress!, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetNoLimiter(IPAddress.Loopback);
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages().RequireRateLimiting(userPolicyName);
app.MapDefaultControllerRoute();
static string GetUserEndPoint(HttpContext context) =>
$"User {context.User.Identity?.Name ?? "Anonymous"} endpoint:{context.Request.Path}"
+ $" {context.Connection.RemoteIpAddress}";
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
.RequireRateLimiting(userPolicyName);
app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
.RequireRateLimiting(helloPolicy);
app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");
app.Run();
Warning
Creating partitions on client IP addresses makes the app vulnerable to Denial of Service Attacks which employ IP Source Address Spoofing. For more information, see BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing.
See the samples repository for the complete Program.cs
file.
The SampleRateLimiterPolicy
class
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using WebRateLimitAuth.Models;
namespace WebRateLimitAuth;
public class SampleRateLimiterPolicy : IRateLimiterPolicy<string>
{
private Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected;
private readonly MyRateLimitOptions _options;
public SampleRateLimiterPolicy(ILogger<SampleRateLimiterPolicy> logger,
IOptions<MyRateLimitOptions> options)
{
_onRejected = (ctx, token) =>
{
ctx.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}");
return ValueTask.CompletedTask;
};
_options = options.Value;
}
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => _onRejected;
public RateLimitPartition<string> GetPartition(HttpContext httpContext)
{
return RateLimitPartition.GetSlidingWindowLimiter(string.Empty,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = _options.PermitLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = _options.QueueLimit,
Window = TimeSpan.FromSeconds(_options.Window),
SegmentsPerWindow = _options.SegmentsPerWindow
});
}
}
In the preceding code, OnRejected uses OnRejectedContext to set the response status to 429 Too Many Requests. The default rejected status is 503 Service Unavailable.
The following sample uses JSON Web Tokens (JWT) and creates a partition with the JWT access token. In a production app, the JWT would typically be provided by a server acting as a Security token service (STS). For local development, the dotnet user-jwts command line tool can be used to create and manage app-specific local JWTs.
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using WebRateLimitAuth.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
var jwtPolicyName = "jwt";
builder.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
limiterOptions.AddPolicy(policyName: jwtPolicyName, partitioner: httpContext =>
{
var accessToken = httpContext.Features.Get<IAuthenticateResultFeature>()?
.AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString()
?? string.Empty;
if (!StringValues.IsNullOrEmpty(accessToken))
{
return RateLimitPartition.GetTokenBucketLimiter(accessToken, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = true
});
});
});
var app = builder.Build();
app.UseAuthorization();
app.UseRateLimiter();
app.MapGet("/", () => "Hello, World!");
app.MapGet("/jwt", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
.RequireRateLimiting(jwtPolicyName)
.RequireAuthorization();
app.MapPost("/post", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
.RequireRateLimiting(jwtPolicyName)
.RequireAuthorization();
app.Run();
static string GetUserEndPointMethod(HttpContext context) =>
$"Hello {context.User.Identity?.Name ?? "Anonymous"} " +
$"Endpoint:{context.Request.Path} Method: {context.Request.Method}";
The following sample:
ConcurrencyLimiter
with a policy name of "get"
that is used on the Razor Pages.TokenBucketRateLimiter
with a partition for each authorized user and a partition for all anonymous users.var getPolicyName = "get";
var postPolicyName = "post";
var myOptions = new MyRateLimitOptions();
builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
builder.Services.AddRateLimiter(_ => _
.AddConcurrencyLimiter(policyName: getPolicyName, options =>
{
options.PermitLimit = myOptions.PermitLimit;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = myOptions.QueueLimit;
})
.AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
{
string userName = httpContext.User.Identity?.Name ?? string.Empty;
if (!StringValues.IsNullOrEmpty(userName))
{
return RateLimitPartition.GetTokenBucketLimiter(userName, _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = myOptions.AutoReplenishment
});
}
return RateLimitPartition.GetTokenBucketLimiter("Anon", _ =>
new TokenBucketRateLimiterOptions
{
TokenLimit = myOptions.TokenLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = myOptions.QueueLimit,
ReplenishmentPeriod = TimeSpan.FromSeconds(myOptions.ReplenishmentPeriod),
TokensPerPeriod = myOptions.TokensPerPeriod,
AutoReplenishment = true
});
}));
See the samples repository for the complete Program.cs
file.
Before deploying an app using rate limiting to production, stress test the app to validate the rate limiters and options used. For example, create a JMeter script with a tool like BlazeMeter or Apache JMeter HTTP(S) Test Script Recorder and load the script to Azure Load Testing.
Creating partitions with user input makes the app vulnerable to Denial of Service (DoS) Attacks. For example, creating partitions on client IP addresses makes the app vulnerable to Denial of Service Attacks that employ IP Source Address Spoofing. For more information, see BCP 38 RFC 2827 Network Ingress Filtering: Defeating Denial of Service Attacks that employ IP Source Address Spoofing.
ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Training
Module
Protect your APIs on Azure API Management - Training
Protect your backend APIs from information exposure and implement throttling (rate limiting) to prevent resource exhaustion with policies in Azure API Management.