AdaptiveRateLimit is an adaptive rate-limiting middleware for ASP.NET Core / .NET 8+ applications.
Author: JOEVER E. MONCEDA
Unlike traditional rate limiting that relies only on a fixed request count, AdaptiveRateLimit evaluates request behavior and risk to determine whether a request should be:
- Allowed
- Throttled
- Blocked
It combines normal request limits, burst detection, endpoint risk, client identity, and progressive penalties to provide an additional layer of application-level protection against abusive traffic.
Important: AdaptiveRateLimit is an application-layer protection mechanism. It should complement, not replace, infrastructure-level security controls such as WAF, CDN, API Gateway, reverse proxy, and dedicated DDoS protection.
- Adaptive rate limiting
- Risk-based request evaluation
- Global rate limiting
- Endpoint-level risk configuration
- Controller-level risk configuration
- Burst detection
- Configurable risk threshold
- Progressive penalties
- Configurable maximum penalty
- Client identity resolution
- Authenticated-user identification
- IP-based identification
- Custom identity resolution
- Optional rate-limit response headers
- ASP.NET Core middleware integration
- .NET 8+ support
- Unit-testable architecture
- NuGet-ready
- .NET 8 or later
- ASP.NET Core
| Framework | Support |
|---|---|
| .NET 8 | ✅ |
| .NET 9 | ✅ |
| .NET 10 | ✅ |
| .NET 7 | ❌ |
| .NET 6 | ❌ |
Install the NuGet package:
dotnet add package AdaptiveRateLimitOr using Visual Studio Package Manager:
Install-Package AdaptiveRateLimitRegister AdaptiveRateLimit in Program.cs:
using AdaptiveRateLimit;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
});
var app = builder.Build();
app.UseAdaptiveRateLimit();
app.MapControllers();
app.Run();The application now has a default rate limit of:
100 requests
within
1 minute
AdaptiveRateLimit can be configured through AddAdaptiveRateLimit.
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
options.BurstLimit = 30;
options.BurstWindow = TimeSpan.FromSeconds(5);
options.BlockRiskScore = 80;
options.InitialPenalty = TimeSpan.FromSeconds(30);
options.MaxPenalty = TimeSpan.FromMinutes(30);
options.IncludeHeaders = true;
options.EnableEndpointRisk = true;
});| Option | Default | Description |
|---|---|---|
PermitLimit |
100 |
Maximum number of requests allowed within the configured Window. |
Window |
1 minute |
Time window used for normal request-rate evaluation. |
BurstLimit |
30 |
Number of requests used to identify burst traffic within the configured BurstWindow. |
BurstWindow |
5 seconds |
Short time window used for burst detection. |
BlockRiskScore |
80 |
Risk score threshold used to determine when a client should be blocked. |
InitialPenalty |
30 seconds |
Initial penalty duration. |
MaxPenalty |
30 minutes |
Maximum penalty duration. |
IncludeHeaders |
true |
Determines whether rate-limit information is included in HTTP response headers. |
EnableEndpointRisk |
true |
Enables endpoint-specific risk evaluation using AdaptiveRateLimitAttribute. |
IdentityResolver |
User/IP | Determines how a client is identified for rate-limit tracking. |
A typical API configuration:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
options.BurstLimit = 30;
options.BurstWindow = TimeSpan.FromSeconds(5);
options.BlockRiskScore = 80;
options.InitialPenalty = TimeSpan.FromSeconds(30);
options.MaxPenalty = TimeSpan.FromMinutes(30);
options.IncludeHeaders = true;
options.EnableEndpointRisk = true;
});
app.UseAdaptiveRateLimit();This provides:
Normal Rate
100 requests / minute
Burst Detection
30 requests / 5 seconds
Block Threshold
Risk Score >= 80
Initial Penalty
30 seconds
Maximum Penalty
30 minutes
AdaptiveRateLimit evaluates incoming requests using multiple signals.
Incoming Request
│
▼
┌────────────────────┐
│ Identify Client │
│ User / IP │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ Request Evaluation │
└─────────┬──────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Request Rate Burst Traffic Endpoint Risk
│ │ │
└────────────────┼────────────────┘
│
▼
Risk Score
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
ALLOW THROTTLE BLOCK
The purpose is to distinguish normal traffic from potentially abusive behavior.
The global configuration establishes the default rate-limit policy.
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
});All requests passing through the middleware are evaluated against this policy.
API
│
▼
AdaptiveRateLimit
│
┌────────────┼────────────┐
▼ ▼ ▼
/products /orders /users
100/min 100/min 100/min
AdaptiveRateLimit supports endpoint-specific risk configuration using:
[AdaptiveRateLimit(Risk = 50)]The important distinction is:
The
Riskproperty on the attribute does not define the request limit.
The request limit is configured using:
options.PermitLimit
options.WindowThe endpoint attribute provides an additional risk value for that endpoint.
Consider a login endpoint:
using AdaptiveRateLimit;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
[AdaptiveRateLimit(Risk = 50)]
public IActionResult Login(LoginRequest request)
{
return Ok();
}
}Global configuration:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
options.BurstLimit = 30;
options.BurstWindow = TimeSpan.FromSeconds(5);
options.BlockRiskScore = 80;
options.EnableEndpointRisk = true;
});The resulting configuration is:
Global Policy
│
├── 100 requests / minute
├── 30 requests / 5 seconds burst detection
└── Block at risk score 80
│
▼
POST /login
│
└── Endpoint Risk = 50
The endpoint risk contributes to the overall risk evaluation.
Different endpoints can have different risk values.
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
[AdaptiveRateLimit(Risk = 50)]
public IActionResult Login(LoginRequest request)
{
return Ok();
}
[HttpPost("register")]
[AdaptiveRateLimit(Risk = 40)]
public IActionResult Register(RegisterRequest request)
{
return Ok();
}
[HttpPost("forgot-password")]
[AdaptiveRateLimit(Risk = 60)]
public IActionResult ForgotPassword(
ForgotPasswordRequest request)
{
return Ok();
}
}Example:
| Endpoint | Risk |
|---|---|
POST /api/auth/login |
50 |
POST /api/auth/register |
40 |
POST /api/auth/forgot-password |
60 |
The request rate itself is still controlled by the global configuration.
AdaptiveRateLimitAttribute can be applied to a controller because the attribute supports both classes and methods.
Example:
[ApiController]
[Route("api/[controller]")]
[AdaptiveRateLimit(Risk = 30)]
public class PaymentController : ControllerBase
{
[HttpPost]
public IActionResult CreatePayment(
PaymentRequest request)
{
return Ok();
}
[HttpPost("confirm")]
public IActionResult ConfirmPayment(
ConfirmPaymentRequest request)
{
return Ok();
}
}The controller establishes a risk value for the controller's endpoints.
A specific endpoint can define its own risk value.
[ApiController]
[Route("api/[controller]")]
[AdaptiveRateLimit(Risk = 30)]
public class PaymentController : ControllerBase
{
[HttpPost]
public IActionResult CreatePayment(
PaymentRequest request)
{
return Ok();
}
[HttpPost("refund")]
[AdaptiveRateLimit(Risk = 70)]
public IActionResult Refund(
RefundRequest request)
{
return Ok();
}
}This allows:
PaymentController
│
├── POST /
│ Risk = 30
│
└── POST /refund
Risk = 70
Endpoint-specific configuration is useful when one operation is more sensitive than other operations in the same controller.
Risk values should be selected according to the application's security requirements.
A possible starting point:
| Endpoint Type | Example Risk |
|---|---|
| Public read-only API | 0–10 |
| Normal API | 10–20 |
| Search | 20–30 |
| Registration | 30–40 |
| Login | 40–50 |
| OTP | 50–60 |
| Password reset | 50–60 |
| Payment | 60–70 |
| Highly sensitive operation | 70+ |
These are recommended starting points only. Applications should tune them according to actual traffic behavior.
Endpoint risk evaluation can be disabled globally:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.EnableEndpointRisk = false;
});When disabled, endpoint-level Risk configuration should not contribute to the rate-limit evaluation.
AdaptiveRateLimit exposes three possible decision types:
public enum RateLimitDecisionType
{
Allow,
Throttle,
Block
}The request is allowed to continue.
Decision: Allow
This represents normal or acceptable traffic.
The request is temporarily restricted.
Decision: Throttle
The client may need to wait before making another request.
The request is blocked because the client has reached a sufficiently high risk level.
Decision: Block
The blocking threshold is configured through:
options.BlockRiskScore = 80;A rate-limit decision contains:
- Decision type
- Risk score
- Reason
- Optional retry duration
Example:
Decision
Block
Risk Score
87
Reason
Client exceeded configured risk threshold
Retry After
30 seconds
The optional retry duration allows clients to determine when they should attempt another request.
AdaptiveRateLimit provides a separate short-duration burst window.
Default configuration:
options.BurstLimit = 30;
options.BurstWindow =
TimeSpan.FromSeconds(5);This allows the middleware to detect unusually high request activity over a short period.
Example:
Normal:
10 requests
│
▼
5 seconds
Burst:
30+ requests
│
▼
5 seconds
Burst behavior contributes to the overall risk evaluation.
AdaptiveRateLimit supports configurable penalties.
Initial penalty:
options.InitialPenalty =
TimeSpan.FromSeconds(30);Maximum penalty:
options.MaxPenalty =
TimeSpan.FromMinutes(30);Conceptually:
Abusive Behavior
│
▼
Initial Penalty
30 seconds
│
▼
Repeated Abuse
│
▼
Longer Penalty
│
▼
Maximum Penalty
30 minutes
The exact penalty behavior is determined by the library's rate-limit evaluation logic.
AdaptiveRateLimit uses IdentityResolver to determine how clients are identified.
The default behavior distinguishes between authenticated users and anonymous clients.
Authenticated users can be represented as:
user:john.doe
Anonymous clients can be represented using their IP:
ip:192.168.1.100
This allows authenticated users to be tracked independently.
Applications can provide a custom identity resolver.
For example, a multi-tenant application can use a tenant claim:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.IdentityResolver = context =>
{
var tenantId =
context.User.FindFirst("tenant_id")?.Value;
return tenantId is not null
? $"tenant:{tenantId}"
: $"ip:{context.Connection.RemoteIpAddress}";
};
});This produces identities such as:
tenant:university-a
tenant:university-b
tenant:company-c
This can be useful for SaaS and multi-tenant applications.
If the application is running behind a reverse proxy, load balancer, or API gateway, forwarded headers should be configured correctly.
using Microsoft.AspNetCore.HttpOverrides;
builder.Services.Configure<ForwardedHeadersOptions>(
options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto;
});Then:
app.UseForwardedHeaders();
app.UseAdaptiveRateLimit();Only trust forwarded headers from infrastructure that you control. Incorrect configuration can allow clients to spoof their identity.
Rate-limit headers can be enabled using:
options.IncludeHeaders = true;This is enabled by default.
To disable rate-limit headers:
options.IncludeHeaders = false;When enabled, the middleware can provide rate-limit information to clients and monitoring systems through HTTP response headers.
AdaptiveRateLimit should generally be placed early in the ASP.NET Core middleware pipeline.
var app = builder.Build();
app.UseAdaptiveRateLimit();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();When using forwarded headers:
var app = builder.Build();
app.UseForwardedHeaders();
app.UseAdaptiveRateLimit();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();Placing rate limiting early allows excessive requests to be rejected before expensive application processing occurs.
using AdaptiveRateLimit;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
options.BurstLimit = 30;
options.BurstWindow = TimeSpan.FromSeconds(5);
options.BlockRiskScore = 80;
options.InitialPenalty =
TimeSpan.FromSeconds(30);
options.MaxPenalty =
TimeSpan.FromMinutes(30);
options.IncludeHeaders = true;
options.EnableEndpointRisk = true;
});
var app = builder.Build();
app.UseAdaptiveRateLimit();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();using AdaptiveRateLimit;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
[AdaptiveRateLimit(Risk = 50)]
public IActionResult Login(LoginRequest request)
{
return Ok();
}
[HttpPost("register")]
[AdaptiveRateLimit(Risk = 40)]
public IActionResult Register(RegisterRequest request)
{
return Ok();
}
[HttpPost("forgot-password")]
[AdaptiveRateLimit(Risk = 60)]
public IActionResult ForgotPassword(
ForgotPasswordRequest request)
{
return Ok();
}
}Global configuration:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
options.BurstLimit = 30;
options.BurstWindow = TimeSpan.FromSeconds(5);
options.BlockRiskScore = 80;
options.EnableEndpointRisk = true;
});The resulting configuration is:
API
│
AdaptiveRateLimit
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Login Register Reset
│ │ │
Risk 50 Risk 40 Risk 60
│ │ │
└────────────┼────────────┘
│
Global Policy
│
100 requests / minute
30 requests / 5 seconds
For Minimal APIs, endpoint metadata can be used with AdaptiveRateLimitAttribute.
Example:
app.MapPost("/api/login", () =>
{
return Results.Ok();
})
.WithMetadata(new AdaptiveRateLimitAttribute
{
Risk = 50
});Another endpoint:
app.MapGet("/api/products", () =>
{
return Results.Ok();
})
.WithMetadata(new AdaptiveRateLimitAttribute
{
Risk = 10
});Global configuration remains the same:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.PermitLimit = 100;
options.Window = TimeSpan.FromMinutes(1);
options.EnableEndpointRisk = true;
});AdaptiveRateLimit can be used to identify traffic by tenant.
Example:
API
│
▼
AdaptiveRateLimit
│
┌───────────┼───────────┐
▼ ▼ ▼
Tenant A Tenant B Tenant C
│ │ │
▼ ▼ ▼
Identity Identity Identity
Using a custom IdentityResolver:
builder.Services.AddAdaptiveRateLimit(options =>
{
options.IdentityResolver = context =>
{
var tenantId =
context.User.FindFirst("tenant_id")?.Value;
return tenantId is not null
? $"tenant:{tenantId}"
: $"ip:{context.Connection.RemoteIpAddress}";
};
});This makes the limiter capable of tracking request behavior according to the application's tenant model.
When running multiple application instances:
Load Balancer
│
┌───────────┼───────────┐
▼ ▼ ▼
Server 1 Server 2 Server 3
│ │ │
▼ ▼ ▼
Limiter Limiter Limiter
If state is maintained in memory, each application instance may maintain its own rate-limit state.
For horizontally scaled deployments, a distributed state mechanism can be considered where supported by the application architecture.
Example:
Server 1 ──┐
Server 2 ──┼──► Distributed State
Server 3 ──┘
AdaptiveRateLimit can help mitigate application-layer HTTP abuse, including request floods.
Example:
Attacker
│
│
HTTP Requests
│
▼
┌──────────────────┐
│ AdaptiveRateLimit│
└────────┬─────────┘
│
Allowed Traffic
│
▼
ASP.NET Core
│
▼
Application
However, application-level rate limiting does not prevent traffic from reaching the network or infrastructure layer.
For production environments, use layered protection.
INTERNET
│
▼
┌──────────────┐
│ CDN │
└──────┬───────┘
│
▼
┌──────────────┐
│ WAF │
└──────┬───────┘
│
▼
┌──────────────┐
│ DDoS Protect │
└──────┬───────┘
│
▼
┌──────────────┐
│ Gateway │
└──────┬───────┘
│
▼
┌──────────────────────┐
│ ASP.NET Core │
│ │
│ AdaptiveRateLimit │
│ │ │
│ ▼ │
│ Controllers │
│ │ │
│ ▼ │
│ Services │
└──────────────────────┘
AdaptiveRateLimit should be considered one layer of a defense-in-depth strategy.
Rate limiting should be combined with:
- Authentication
- Authorization
- Input validation
- WAF
- DDoS protection
- API Gateway
- Network security
- Bot protection
- Logging
- Monitoring
Particular attention should be given to sensitive operations:
POST /login
POST /register
POST /otp
POST /password/reset
POST /payment
POST /checkout
POST /refund
Endpoint risk can be used to assign higher risk values to these operations.
Rate limiting executes for incoming requests, so the implementation should remain lightweight.
Recommended practices:
- Avoid database queries for every request
- Avoid unnecessary allocations
- Keep identity resolution lightweight
- Use appropriate state storage
- Monitor middleware execution time
- Monitor memory consumption
- Monitor throttled and blocked requests
- Monitor high-risk identities
Recommended metrics include:
Total Requests
│
├── Allowed
│
├── Throttled
│
├── Blocked
│
├── Burst Events
│
├── Risk Scores
│
└── Penalty Events
Useful metrics include:
- Total requests
- Requests per identity
- Requests per endpoint
- Requests per tenant
- Allowed requests
- Throttled requests
- Blocked requests
- HTTP 429 responses
- Risk scores
- Burst events
- Penalty events
- Penalty duration
- Top high-risk identities
Run the complete test suite using:
dotnet testRecommended test scenarios include:
- Requests within the configured limit
- Requests exceeding the configured limit
- Burst detection
- Risk score evaluation
- High-risk requests
- Blocking behavior
- Throttling behavior
- Penalty behavior
- Penalty expiration
- Endpoint risk
- Controller risk
- Global configuration
- Custom identity resolution
- Rate-limit headers
Request
│
▼
Within Limit
│
▼
ALLOW
Request
│
▼
Rate Exceeded
│
▼
THROTTLE
Request
│
▼
Risk Evaluation
│
▼
Risk >= BlockRiskScore
│
▼
BLOCK
Many Requests
│
▼
BurstWindow
│
▼
Burst Detected
│
▼
Risk Increased
A decision can contain information such as:
Decision:
Block
Risk Score:
87
Reason:
Client exceeded configured risk threshold.
Retry After:
30 seconds
The RetryAfter value is optional and can be used by clients to determine when they should retry.
AdaptiveRateLimit is suitable for:
- REST APIs
- ASP.NET Core applications
- SaaS applications
- Multi-tenant systems
- Mobile application backends
- Public APIs
- Internal enterprise APIs
- High-traffic applications
- APIs with unpredictable traffic
- Applications requiring application-level abuse protection
Do not rely solely on AdaptiveRateLimit for public-facing DDoS protection.
A basic architecture:
Internet
│
▼
ASP.NET Core
│
▼
AdaptiveRateLimit
provides application-level protection, but a stronger architecture is:
Internet
│
▼
CDN
│
▼
WAF
│
▼
DDoS Protection
│
▼
Gateway
│
▼
AdaptiveRateLimit
│
▼
ASP.NET Core
The outer layers can block unwanted traffic before it consumes application resources.
Potential future capabilities include:
- Distributed rate-limit state
- Redis state provider
- Tenant-aware rate limiting
- API-key identity resolution
- Advanced anomaly detection
- OpenTelemetry integration
- Prometheus metrics
- Rate-limit dashboard
- Configurable risk algorithms
- Automatic attack-pattern detection
- Dynamic tenant quotas
- Endpoint policy groups
Contributions are welcome.
Before submitting a pull request:
- Create a feature branch.
- Implement the change.
- Add or update tests.
- Run the test suite.
- Run the build.
- Ensure all tests pass.
- Submit a pull request.
dotnet restore
dotnet build
dotnet testThis project is licensed under the terms specified in the repository's license.
AdaptiveRateLimit provides application-level rate limiting and abuse mitigation.
It is not a replacement for:
- Dedicated DDoS protection
- Web Application Firewalls
- CDN protection
- Network security controls
- API gateways
- Cloud infrastructure security
The appropriate configuration depends on the application's traffic profile, architecture, infrastructure, and security requirements.
AdaptiveRateLimit follows four primary principles:
Normal Traffic
│
▼
ALLOW
│
│
┌─────────┴─────────┐
│ │
▼ ▼
Higher Risk Excessive Risk
│ │
▼ ▼
THROTTLE BLOCK
│ │
└─────────┬─────────┘
▼
Gradual Recovery
The objective is not simply to block requests.
The objective is to:
Allow legitimate traffic, detect abnormal behavior, throttle suspicious activity, block high-risk clients, and recover when traffic returns to normal.
AdaptiveRateLimit provides a lightweight adaptive protection layer that can be integrated directly into modern ASP.NET Core applications.