Asp.Net Core Security
Rate Limit
Created: 24 Jan 2026
Updated: 24 Jan 2026
Chained Rate Limiting Strategy
Table of Contents
- Introduction
- How It Works
- Implementation
- Combination Patterns
- Real-World Examples
- When to Use
- Complete Example
Introduction
Chained Rate Limiting applies multiple limiters in sequence. A request must pass ALL limiters to be processed. This provides defense in depth for critical endpoints.
The Concept:
Single Limiter:
Request → [Rate Limiter] → Process or Reject
Chained Limiters:
Request → [Limiter 1] → [Limiter 2] → [Limiter 3] → Process
↓ PASS ↓ PASS ↓ PASS
If ANY fails → REJECT
Why Chain Limiters?
- ✅ Multiple constraints - Enforce different limits simultaneously
- ✅ Defense in depth - Layered protection
- ✅ Comprehensive control - Rate + Concurrency + Per-User
- ✅ Critical endpoint protection - Payments, admin operations
How It Works
Sequential Checking
Chained Limiter Flow:
Request arrives
↓
┌───────────────────┐
│ Limiter 1: │
│ Concurrency (5) │
│ Check: Active < 5?│
└────────┬──────────┘
│ PASS
↓
┌───────────────────┐
│ Limiter 2: │
│ Per-User (30/min) │
│ Check: User < 30? │
└────────┬──────────┘
│ PASS
↓
┌───────────────────┐
│ Limiter 3: │
│ Global (100/min) │
│ Check: Total <100?│
└────────┬──────────┘
│ PASS
↓
Process Request ✓
If ANY check fails → REJECT ❌
AND Logic
Result = Limiter1 AND Limiter2 AND Limiter3
Examples:
Scenario 1:
Limiter1: PASS ✓
Limiter2: PASS ✓
Limiter3: PASS ✓
Result: ALLOW ✓
Scenario 2:
Limiter1: PASS ✓
Limiter2: FAIL ❌
Limiter3: Not checked
Result: REJECT ❌
Scenario 3:
Limiter1: FAIL ❌
Limiter2: Not checked
Limiter3: Not checked
Result: REJECT ❌
Implementation
Method 1: Multiple .RequireRateLimiting() Calls
// ASP.NET Core applies limiters in order
app.MapPost("/api/payment", ProcessPayment)
.RequireRateLimiting("concurrency") // Check 1: Max 3 concurrent
.RequireRateLimiting("per-user") // Check 2: 10 per minute per user
.RequireRateLimiting("global"); // Check 3: 100 per minute global
// Execution order:
// 1. Concurrency check
// 2. Per-user check
// 3. Global check
// All must pass!
Method 2: Custom Chained Policy
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
namespace SecurityApp.API.RateLimiting;
/// <summary>
/// Chained Rate Limiter
/// Combines multiple limiters for comprehensive protection.
/// </summary>
public static class ChainedRateLimiter
{
public const string PolicyName = "Chained";
/// <summary>
/// Payment Processing Chain:
/// 1. Concurrency: Max 3 simultaneous payments
/// 2. Per-User: 10 payments per minute per user
/// 3. Global: 100 payments per minute across all users
///
/// All three limits must be satisfied.
/// </summary>
public static void Configure(RateLimiterOptions options)
{
// Layer 1: Concurrency protection
options.AddConcurrencyLimiter("payment-concurrency", opt =>
{
opt.PermitLimit = 3;
opt.QueueLimit = 2;
});
// Layer 2: Per-user protection
options.AddPolicy("payment-per-user", context =>
{
var userId = context.User.Identity?.Name ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
userId,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
});
});
// Layer 3: Global protection
options.AddFixedWindowLimiter("payment-global", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
}
}
Registration
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
ChainedRateLimiter.Configure(options);
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = "One or more rate limits exceeded",
strategy = "chained",
hint = "Multiple protection layers enforced"
}, ct);
};
});
var app = builder.Build();
app.UseRateLimiter();
// Apply chained limiters
app.MapPost("/api/payment", ProcessPayment)
.RequireRateLimiting("payment-concurrency")
.RequireRateLimiting("payment-per-user")
.RequireRateLimiting("payment-global");
app.Run();
Combination Patterns
Pattern 1: Concurrency + Rate
// Protect resource-intensive operations
// Step 1: Limit concurrent operations
options.AddConcurrencyLimiter("report-concurrency", opt =>
{
opt.PermitLimit = 2; // Max 2 concurrent
opt.QueueLimit = 5;
});
// Step 2: Limit rate per user
options.AddPolicy("report-per-user", context =>
{
var userId = GetUserId(context);
return RateLimitPartition.GetFixedWindowLimiter(
userId,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10, // 10 reports per hour per user
Window = TimeSpan.FromHours(1)
});
});
// Apply both
app.MapPost("/api/reports/generate", GenerateReport)
.RequireRateLimiting("report-concurrency")
.RequireRateLimiting("report-per-user");
// Result:
// - Max 2 reports generating simultaneously
// - Max 10 reports per hour per user
// - Both enforced independently
Pattern 2: Per-User + Global
// Fair allocation with global ceiling
// Step 1: Per-user fairness
options.AddPolicy("fair-per-user", context =>
{
var userId = GetUserId(context);
return RateLimitPartition.GetSlidingWindowLimiter(
userId,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 50, // 50 per minute per user
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
});
});
// Step 2: Global protection
options.AddSlidingWindowLimiter("global-ceiling", opt =>
{
opt.PermitLimit = 1000; // 1000 per minute global
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6;
});
// Apply both
app.MapGet("/api/data", GetData)
.RequireRateLimiting("fair-per-user")
.RequireRateLimiting("global-ceiling");
// Result:
// - Each user can make 50 req/min
// - But total across all users capped at 1000 req/min
Pattern 3: Token Bucket + Concurrency
// Bursty traffic with resource protection
// Step 1: Allow bursts
options.AddTokenBucketLimiter("burst", opt =>
{
opt.TokenLimit = 50;
opt.TokensPerPeriod = 10;
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
opt.AutoReplenishment = true;
});
// Step 2: Protect resources
options.AddConcurrencyLimiter("resource", opt =>
{
opt.PermitLimit = 10;
opt.QueueLimit = 20;
});
// Apply both
app.MapPost("/api/upload", UploadFile)
.RequireRateLimiting("burst")
.RequireRateLimiting("resource");
// Result:
// - Initial burst of 50 uploads
// - But max 10 concurrent uploads
// - Balanced burst + resource protection
Real-World Examples
Example 1: Payment Processing
// Critical endpoint with multiple protections
public static void ConfigurePaymentChain(RateLimiterOptions options)
{
// Layer 1: Concurrency (payment gateway limit)
options.AddConcurrencyLimiter("payment-gateway", opt =>
{
opt.PermitLimit = 5; // Gateway supports 5 concurrent
opt.QueueLimit = 10;
});
// Layer 2: Per-user (prevent rapid retries)
options.AddPolicy("payment-user", context =>
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
userId,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5, // 5 payments per minute per user
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0 // No queue for payments
});
});
// Layer 3: Global (fraud prevention)
options.AddSlidingWindowLimiter("payment-global", opt =>
{
opt.PermitLimit = 100; // 100 payments per minute globally
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6;
});
}
app.MapPost("/api/payment/process", async (PaymentRequest request) =>
{
var result = await _paymentService.ProcessAsync(request);
return Results.Ok(result);
})
.RequireAuthorization()
.RequireRateLimiting("payment-gateway")
.RequireRateLimiting("payment-user")
.RequireRateLimiting("payment-global");
Example 2: Admin Operations
// Protect admin endpoints with strict limits
public static void ConfigureAdminChain(RateLimiterOptions options)
{
// Layer 1: Very strict per-admin
options.AddPolicy("admin-user", context =>
{
var adminId = context.User.FindFirst("admin_id")?.Value ?? "unknown";
return RateLimitPartition.GetFixedWindowLimiter(
adminId,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 20, // 20 operations per hour
Window = TimeSpan.FromHours(1)
});
});
// Layer 2: Concurrency
options.AddConcurrencyLimiter("admin-concurrent", opt =>
{
opt.PermitLimit = 3; // Max 3 concurrent admin ops
opt.QueueLimit = 5;
});
// Layer 3: Global daily limit
options.AddFixedWindowLimiter("admin-daily", opt =>
{
opt.PermitLimit = 500; // 500 admin operations per day
opt.Window = TimeSpan.FromDays(1);
});
}
app.MapDelete("/api/admin/users/{id:int}", DeleteUser)
.RequireAuthorization(policy => policy.RequireRole("Admin"))
.RequireRateLimiting("admin-user")
.RequireRateLimiting("admin-concurrent")
.RequireRateLimiting("admin-daily");
Example 3: API Gateway
// Multi-layer protection for public API
public static void ConfigureApiGateway(RateLimiterOptions options)
{
// Layer 1: Per-IP (DDoS protection)
options.AddPolicy("ip-ddos", context =>
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetSlidingWindowLimiter(
ip,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
});
});
// Layer 2: Per-API-Key (customer limits)
options.AddPolicy("api-key", context =>
{
var apiKey = context.Request.Headers["X-API-Key"].FirstOrDefault() ?? "no-key";
return RateLimitPartition.GetTokenBucketLimiter(
apiKey,
_ => new TokenBucketRateLimiterOptions
{
TokenLimit = 1000,
TokensPerPeriod = 200,
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
AutoReplenishment = true
});
});
// Layer 3: Global (server protection)
options.AddConcurrencyLimiter("server-capacity", opt =>
{
opt.PermitLimit = 1000; // Max 1000 concurrent requests
opt.QueueLimit = 500;
});
}
app.MapGet("/api/v1/data", GetData)
.RequireRateLimiting("ip-ddos")
.RequireRateLimiting("api-key")
.RequireRateLimiting("server-capacity");
When to Use
✅ Perfect For
1. Payment Processing
// Multiple constraints for safety
app.MapPost("/api/payment", ProcessPayment)
.RequireRateLimiting("concurrency")
.RequireRateLimiting("per-user")
.RequireRateLimiting("global");
2. Admin Operations
// Strict control on privileged actions
app.MapDelete("/api/admin/delete", Delete)
.RequireRateLimiting("admin-rate")
.RequireRateLimiting("admin-concurrency");
3. High-Value Transactions
// Critical business operations
app.MapPost("/api/transfer", Transfer)
.RequireRateLimiting("chain");
4. Compliance Requirements
// Meet regulatory limits
app.MapGet("/api/sensitive", GetSensitiveData)
.RequireRateLimiting("compliance-chain");
❌ Avoid When
1. Simple Read Operations
// Overkill for basic GET
app.MapGet("/api/products", GetProducts)
.RequireRateLimiting("SlidingWindow"); // One limiter enough
2. Internal APIs
// Trusted internal services
app.MapGet("/internal/status", GetStatus)
.RequireRateLimiting("FixedWindow"); // Simple is better
3. High-Traffic, Low-Risk
// Public search, low risk
app.MapGet("/api/search", Search)
.RequireRateLimiting("TokenBucket"); // Single limiter OK
Complete Example
Full Production Setup
// Program.cs
using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddRateLimiter(options =>
{
// === Payment Chain ===
// Payment: Concurrency
options.AddConcurrencyLimiter("payment-concurrent", opt =>
{
opt.PermitLimit = 5;
opt.QueueLimit = 10;
});
// Payment: Per-User
options.AddPolicy("payment-user", context =>
{
var userId = context.User.Identity?.Name ?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
userId,
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
});
});
// Payment: Global
options.AddSlidingWindowLimiter("payment-global", opt =>
{
opt.PermitLimit = 200;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6;
});
// === Admin Chain ===
// Admin: Per-Admin
options.AddPolicy("admin-user", context =>
{
var adminId = context.User.FindFirst("sub")?.Value ?? "unknown";
return RateLimitPartition.GetFixedWindowLimiter(
$"admin:{adminId}",
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 30,
Window = TimeSpan.FromHours(1)
});
});
// Admin: Concurrency
options.AddConcurrencyLimiter("admin-concurrent", opt =>
{
opt.PermitLimit = 3;
opt.QueueLimit = 5;
});
// Rejection handler
options.OnRejected = async (context, ct) =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
logger.LogWarning(
"Chained rate limit exceeded: User={User}, Path={Path}",
context.HttpContext.User.Identity?.Name ?? "Anonymous",
context.HttpContext.Request.Path);
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = "One or more rate limit layers exceeded",
strategy = "chained",
hint = "Multiple protection layers are enforced on this endpoint"
}, ct);
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();
// Payment endpoint: Triple protection
app.MapPost("/api/payment/process", async (PaymentRequest request) =>
{
await _paymentService.ProcessAsync(request);
return Results.Ok(new { success = true });
})
.RequireAuthorization()
.RequireRateLimiting("payment-concurrent")
.RequireRateLimiting("payment-user")
.RequireRateLimiting("payment-global");
// Admin endpoint: Double protection
app.MapDelete("/api/admin/users/{id:int}", async (int id) =>
{
await _userService.DeleteAsync(id);
return Results.Ok();
})
.RequireAuthorization(policy => policy.RequireRole("Admin"))
.RequireRateLimiting("admin-user")
.RequireRateLimiting("admin-concurrent");
app.Run();
record PaymentRequest(string CardNumber, decimal Amount);
Design Your Chain
Questions to ask:
1. What resource needs protection?
→ Add Concurrency limiter
2. Should each user have a limit?
→ Add Per-User limiter
3. Is there a global capacity?
→ Add Global limiter
4. Need burst handling?
→ Add Token Bucket limiter
Apply all relevant limiters!