Asp.Net Core Security
Rate Limit
Created: 24 Jan 2026
Updated: 24 Jan 2026
Sliding Window Rate Limiting Strategy
Table of Contents
- Introduction
- How It Works
- Implementation
- Mathematical Analysis
- Solving the Boundary Problem
- Production Setup
- Performance Considerations
- When to Use
- Complete Example
Introduction
Sliding Window is the production-ready rate limiting strategy. It solves Fixed Window's boundary burst problem while maintaining reasonable complexity and memory usage.
The Problem It Solves:
Fixed Window Issue:
00:59:50 → 10 requests ✓
01:00:01 → 10 requests ✓
Result: 20 requests in 11 seconds! 💥
Sliding Window Solution:
00:59:50 → 10 requests ✓
01:00:01 → Only 5 allowed (weighted by overlap)
Result: 15 requests in 11 seconds ✓
Why Choose Sliding Window?
- ✅ Prevents boundary bursts - Smooth traffic distribution
- ✅ Production-ready - Used by major APIs (Twitter, GitHub)
- ✅ Good performance - Only slightly slower than Fixed Window
- ✅ Easy upgrade - Drop-in replacement for Fixed Window
Related Articles
- Previous: Fixed Window Strategy
- Next: Token Bucket Strategy
- Compare: Strategy Comparison
How It Works
Core Concept
Instead of hard window boundaries, Sliding Window weights requests from the previous window based on overlap.
Visual Representation:
Time: 00:00:35 (35 seconds into current window)
Previous Window [23:59:00 - 00:00:00]: 12 requests
Current Window [00:00:00 - 00:01:00]: 8 requests
Overlap calculation:
- We're 35/60 = 58.3% into current window
- Previous window contributes: (1 - 0.583) = 41.7%
Weighted count = (12 × 0.417) + 8 = 5 + 8 = 13 requests
If limit is 15:
Available = 15 - 13 = 2 more requests allowed
Algorithm
For each request at time T:
1. Calculate current window ID
current_window = floor(T / window_size)
2. Calculate overlap percentage
time_in_window = T % window_size
overlap = time_in_window / window_size
3. Get previous window count
previous_window = current_window - 1
prev_count = count[previous_window]
4. Calculate weighted total
weighted_count = (prev_count × (1 - overlap)) + count[current_window]
5. Check limit
if weighted_count < limit:
count[current_window]++
return ALLOW
else:
return REJECT
Segment-Based Approach
ASP.NET Core uses segments for implementation:
Window divided into 6 segments (10 seconds each):
[Seg 1][Seg 2][Seg 3][Seg 4][Seg 5][Seg 6]
[0-10s][10-20][20-30][30-40][40-50][50-60]
At 35 seconds:
- Segment 4 is current
- Segments 1-3 have decreasing weights
- Segments 5-6 not yet counted
Weight calculation:
Seg 1: 0% (too old)
Seg 2: 16.7% weight
Seg 3: 50% weight
Seg 4: 100% weight (current)
Implementation
Complete C# Implementation
using Microsoft.AspNetCore.RateLimiting;
namespace SecurityApp.API.RateLimiting;
/// <summary>
/// Sliding Window Rate Limiter
/// Production-ready strategy that prevents boundary burst attacks.
/// </summary>
public static class SlidingWindowRateLimiter
{
public const string PolicyName = "SlidingWindow";
/// <summary>
/// Configuration:
/// - Window: 1 minute
/// - PermitLimit: 15 requests per window
/// - SegmentsPerWindow: 6 (10-second segments)
///
/// How it works:
/// Each window is divided into 6 segments of 10 seconds.
/// Requests from previous segments are weighted by time overlap.
///
/// Example:
/// If you made 10 requests in the last 30 seconds of previous window,
/// you can only make 5 more in the first 30 seconds of current window.
/// </summary>
public static void Configure(RateLimiterOptions rateLimiterOptions)
{
rateLimiterOptions.AddSlidingWindowLimiter(PolicyName, options =>
{
options.PermitLimit = 15;
options.Window = TimeSpan.FromMinutes(1);
options.SegmentsPerWindow = 6; // Creates 6 × 10-second segments
options.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
options.QueueLimit = 3;
});
}
}
Registration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
// Register Sliding Window
SlidingWindowRateLimiter.Configure(options);
// Configure rejection behavior
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = 429;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = "Rate limit exceeded. Requests are smoothly distributed.",
retry_after_seconds = (int)retryAfter.TotalSeconds,
strategy = "sliding_window"
}, cancellationToken);
};
});
var app = builder.Build();
app.UseRateLimiter();
// Apply to endpoints
app.MapGet("/api/products", () => GetProducts())
.RequireRateLimiting(SlidingWindowRateLimiter.PolicyName);
app.Run();
Choosing Segment Count
// More segments = Smoother, but higher memory usage
// 2 segments (30-second segments)
options.SegmentsPerWindow = 2; // Memory: Low, Smoothness: Low
// 6 segments (10-second segments) - RECOMMENDED
options.SegmentsPerWindow = 6; // Memory: Medium, Smoothness: Good
// 12 segments (5-second segments)
options.SegmentsPerWindow = 12; // Memory: High, Smoothness: Excellent
// 60 segments (1-second segments)
options.SegmentsPerWindow = 60; // Memory: Very High, Smoothness: Maximum
Rule of Thumb:
Segments = Window in seconds / Desired granularity
For 1-minute window:
- 6 segments = 10-second granularity (good for most cases)
- 12 segments = 5-second granularity (high traffic APIs)
- 30 segments = 2-second granularity (ultra-precise)
Mathematical Analysis
Detailed Calculation Example
Scenario:
- Window: 60 seconds
- Segments: 6 (10 seconds each)
- Limit: 15 requests/minute
- Current time: 00:00:35
Previous Window (23:59:00 - 00:00:00):
Segment 1 (23:59:00-23:59:10): 2 requests
Segment 2 (23:59:10-23:59:20): 3 requests
Segment 3 (23:59:20-23:59:30): 2 requests
Segment 4 (23:59:30-23:59:40): 4 requests
Segment 5 (23:59:40-23:59:50): 2 requests
Segment 6 (23:59:50-00:00:00): 2 requests
Total: 15 requests
Current Window (00:00:00 - 00:01:00):
Segment 1 (00:00:00-00:00:10): 3 requests
Segment 2 (00:00:10-00:00:20): 2 requests
Segment 3 (00:00:20-00:00:30): 4 requests
Segment 4 (00:00:30-00:00:40): Currently at 00:00:35 → 1 request so far
Calculation at 00:00:35:
Step 1: Calculate position in window
Time in window = 35 seconds
Percentage = 35 / 60 = 0.583 (58.3%)
Step 2: Calculate overlap with previous window
Overlap percentage = 1 - 0.583 = 0.417 (41.7%)
Step 3: Weight previous window
Previous count = 15 requests
Weighted previous = 15 × 0.417 = 6.25
Step 4: Count current window
Current count = 3 + 2 + 4 + 1 = 10 requests
Step 5: Total weighted count
Total = 6.25 + 10 = 16.25
Step 6: Check limit
Limit = 15
Available = 15 - 16.25 = -1.25
Result: REJECT (over limit by 1.25 requests)
Comparison: Fixed vs Sliding Window
Test Case: Boundary Burst Attack
Attack Pattern:
00:59:50 → Send 10 requests
01:00:01 → Send 10 requests
Fixed Window Response:
00:59:50 → 10/10 used ✓
01:00:00 → Counter resets
01:00:01 → 10/10 used ✓
Result: 20 requests in 11 seconds! 💥
Sliding Window Response:
00:59:50 → 10 requests ✓
At 01:00:01 (1 second into new window):
Overlap = 1 / 60 = 1.67%
Previous weight = (1 - 0.0167) = 98.3%
Weighted previous = 10 × 0.983 = 9.83
Current count = 0
Total = 9.83
Available = 15 - 9.83 = 5.17
01:00:01 → Only 5 requests allowed ✓
Result: 15 requests in 11 seconds ✓
Memory Usage Calculation
Per Segment State:
- Segment ID: 4 bytes
- Request count: 4 bytes
- Timestamp: 8 bytes
Total per segment: 16 bytes
For 6 segments per window:
Memory per user = 6 × 16 = 96 bytes
For 1,000 concurrent users:
Total memory = 1,000 × 96 = 96 KB
For 1,000,000 users:
Total memory = 1,000,000 × 96 = 96 MB
Comparison:
Fixed Window: 20 bytes per user = 20 MB for 1M users
Sliding Window: 96 bytes per user = 96 MB for 1M users
Increase: 4.8x (acceptable trade-off for boundary protection)
Solving the Boundary Problem
The Fixed Window Flaw
Problem Visualization:
Fixed Window (10 requests/minute):
[Window 1: 00:59:00-00:59:59]
... 59:50 59:55 59:59
Requests: ... ✓ ✓ ✓ (10 total)
[Window 2: 01:00:00-01:00:59]
00:00 00:05 00:10 ...
Requests: ✓ ✓ ✓ (10 total)
Burst at boundary:
59:50-00:10 = 20 seconds
Requests = 20
Rate = 60 req/min (6x over limit!)
Sliding Window Solution
Sliding Window (15 requests/minute, 6 segments):
At 59:50 (50 seconds into Window 1):
Previous window: 0
Current window: 10
Weighted: 0 × 0.17 + 10 = 10 ✓ Allow
At 00:00 (0 seconds into Window 2):
Previous window: 10
Current window: 0
Weighted: 10 × 1.0 + 0 = 10 ✓ Still counting
At 00:01 (1 second into Window 2):
Previous window: 10
Current window: 0
Weighted: 10 × 0.983 + 0 = 9.83
Available: 15 - 9.83 = 5.17
Only 5 more requests allowed ✓
At 00:10 (10 seconds into Window 2):
Previous window: 10
Current window: 5
Weighted: 10 × 0.833 + 5 = 13.33
Available: 15 - 13.33 = 1.67
Only 1 more request allowed ✓
Result: Maximum 16 requests in 20 seconds (acceptable)
vs Fixed Window: 20 requests in 20 seconds (unacceptable)
Real-World Protection
GitHub API Example:
// GitHub uses Sliding Window for their API
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("github-api", opt =>
{
opt.PermitLimit = 5000; // 5,000 requests
opt.Window = TimeSpan.FromHours(1); // per hour
opt.SegmentsPerWindow = 60; // 1-minute segments
});
});
// Benefits:
// - Attackers can't exploit boundary
// - Legitimate users get smooth experience
// - No sudden rejections at hour boundaries
Production Setup
Multi-Tier Configuration
builder.Services.AddRateLimiter(options =>
{
// Public API: Moderate limit
options.AddSlidingWindowLimiter("public-api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6;
opt.QueueLimit = 10;
});
// Authenticated users: Higher limit
options.AddSlidingWindowLimiter("authenticated", opt =>
{
opt.PermitLimit = 1000;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 12; // Smoother for high traffic
opt.QueueLimit = 50;
});
// Authentication endpoint: Very strict
options.AddSlidingWindowLimiter("auth", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6;
opt.QueueLimit = 0; // No queue for auth
});
// Heavy operations: Tight control
options.AddSlidingWindowLimiter("heavy", opt =>
{
opt.PermitLimit = 10;
opt.Window = TimeSpan.FromMinutes(5);
opt.SegmentsPerWindow = 30; // 10-second segments
opt.QueueLimit = 5;
});
});
Dynamic Configuration from Settings
// appsettings.json
{
"RateLimiting": {
"SlidingWindow": {
"PublicApi": {
"PermitLimit": 100,
"WindowMinutes": 1,
"SegmentsPerWindow": 6,
"QueueLimit": 10
},
"Authenticated": {
"PermitLimit": 1000,
"WindowMinutes": 1,
"SegmentsPerWindow": 12,
"QueueLimit": 50
}
}
}
}
// Configuration class
public class SlidingWindowConfig
{
public int PermitLimit { get; set; }
public int WindowMinutes { get; set; }
public int SegmentsPerWindow { get; set; }
public int QueueLimit { get; set; }
}
// Load and apply
var publicConfig = builder.Configuration
.GetSection("RateLimiting:SlidingWindow:PublicApi")
.Get<SlidingWindowConfig>();
builder.Services.AddRateLimiter(options =>
{
options.AddSlidingWindowLimiter("public-api", opt =>
{
opt.PermitLimit = publicConfig.PermitLimit;
opt.Window = TimeSpan.FromMinutes(publicConfig.WindowMinutes);
opt.SegmentsPerWindow = publicConfig.SegmentsPerWindow;
opt.QueueLimit = publicConfig.QueueLimit;
});
});
Comprehensive Error Response
options.OnRejected = async (context, cancellationToken) =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
var endpoint = context.HttpContext.GetEndpoint()?.DisplayName ?? "Unknown";
var ip = context.HttpContext.Connection.RemoteIpAddress;
logger.LogWarning(
"Rate limit exceeded: Endpoint={Endpoint}, IP={IP}, Path={Path}",
endpoint, ip, context.HttpContext.Request.Path);
context.HttpContext.Response.StatusCode = 429;
context.HttpContext.Response.Headers.ContentType = "application/json";
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
}
var response = new
{
error = "rate_limit_exceeded",
message = "You have exceeded the rate limit for this endpoint.",
details = new
{
strategy = "sliding_window",
explanation = "Requests are smoothly distributed to prevent bursts",
retry_after_seconds = (int?)retryAfter?.TotalSeconds ?? 60,
endpoint = context.HttpContext.Request.Path.Value,
timestamp = DateTime.UtcNow,
documentation = "https://api.example.com/docs/rate-limits"
},
suggestions = new[]
{
"Implement exponential backoff",
"Cache responses when possible",
"Consider upgrading your plan for higher limits"
}
};
await context.HttpContext.Response.WriteAsJsonAsync(response, cancellationToken);
};
Memory Optimization
// Optimize for high-traffic scenarios
// Option 1: Reduce segments for memory savings
options.AddSlidingWindowLimiter("optimized", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 3; // 20-second segments (50% memory reduction)
});
// Option 2: Longer windows with same segments
options.AddSlidingWindowLimiter("long-window", opt =>
{
opt.PermitLimit = 600;
opt.Window = TimeSpan.FromMinutes(10);
opt.SegmentsPerWindow = 6; // 100-second segments
});
// Option 3: Partitioned by endpoint (not user)
options.AddPolicy("per-endpoint", context =>
{
var endpoint = context.GetEndpoint()?.DisplayName ?? "default";
return RateLimitPartition.GetSlidingWindowLimiter(
endpoint, // Partition by endpoint, not user
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
});
});
When to Use
✅ Perfect For
1. Production Public APIs
// Recommended for all production external APIs
app.MapGet("/api/v1/users", () => GetUsers())
.RequireRateLimiting("SlidingWindow");
// Why: Prevents boundary exploits, smooth user experience
2. High-Security Endpoints
// Authentication, payments, sensitive data
app.MapPost("/api/auth/login", (LoginRequest req) => Login(req))
.RequireRateLimiting("SlidingWindow");
app.MapPost("/api/payments", (PaymentRequest req) => ProcessPayment(req))
.RequireRateLimiting("SlidingWindow");
// Why: No boundary vulnerabilities for critical operations
3. E-Commerce Platforms
// Flash sales, checkout, inventory
app.MapPost("/api/cart/checkout", (CheckoutRequest req) => Checkout(req))
.RequireRateLimiting("SlidingWindow");
// Why: Handles traffic spikes smoothly without boundary bursts
4. APIs with SLAs
// Guaranteed rate limits in SLA
app.MapGet("/api/enterprise/data", () => GetData())
.RequireRateLimiting("SlidingWindow");
// Why: Accurate enforcement, no boundary loopholes
❌ Consider Alternatives When
1. Internal Low-Traffic APIs
// Use Fixed Window for simplicity
app.MapGet("/internal/health", () => "OK")
.RequireRateLimiting("FixedWindow"); // Simpler is better
// Why: Overhead not worth it for trusted, low-traffic internal APIs
2. Extremely High Traffic
// Consider Token Bucket for better burst handling
app.MapGet("/api/cdn/assets/{id}", (int id) => GetAsset(id))
.RequireRateLimiting("TokenBucket");
// Why: Token Bucket allows controlled bursts more naturally
3. Long-Running Operations
// Use Concurrency Limiter instead
app.MapPost("/api/reports/generate", (ReportRequest req) => GenerateReport(req))
.RequireRateLimiting("Concurrency");
// Why: Limits simultaneous operations, not rate
Complete Example
Full Production Implementation
// Program.cs
using Microsoft.AspNetCore.RateLimiting;
using SecurityApp.API.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// Load rate limit configuration
var rateLimitConfig = builder.Configuration.GetSection("RateLimiting");
builder.Services.AddRateLimiter(options =>
{
// Public API: Sliding Window
options.AddSlidingWindowLimiter("public-api", opt =>
{
opt.PermitLimit = rateLimitConfig.GetValue<int>("PublicApi:Limit", 100);
opt.Window = TimeSpan.FromMinutes(
rateLimitConfig.GetValue<int>("PublicApi:WindowMinutes", 1));
opt.SegmentsPerWindow = rateLimitConfig.GetValue<int>("PublicApi:Segments", 6);
opt.QueueLimit = rateLimitConfig.GetValue<int>("PublicApi:QueueLimit", 10);
opt.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
});
// Authenticated: Higher limits, more segments
options.AddSlidingWindowLimiter("authenticated", opt =>
{
opt.PermitLimit = 1000;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 12; // Smoother for high traffic
opt.QueueLimit = 50;
});
// Per-user limits
options.AddPolicy("per-user", context =>
{
var userId = context.User.Identity?.Name ??
context.Connection.RemoteIpAddress?.ToString() ??
"anonymous";
return RateLimitPartition.GetSlidingWindowLimiter(
userId,
_ => new System.Threading.RateLimiting.SlidingWindowRateLimiterOptions
{
PermitLimit = 50,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6,
QueueLimit = 5,
QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst
});
});
// Comprehensive rejection handling
options.OnRejected = async (context, ct) =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
logger.LogWarning(
"Rate limit exceeded: Path={Path}, IP={IP}, User={User}",
context.HttpContext.Request.Path,
context.HttpContext.Connection.RemoteIpAddress,
context.HttpContext.User.Identity?.Name ?? "Anonymous");
context.HttpContext.Response.StatusCode = 429;
context.HttpContext.Response.Headers.ContentType = "application/json";
if (context.Lease.TryGetMetadata(
System.Threading.RateLimiting.MetadataName.RetryAfter,
out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
context.HttpContext.Response.Headers.Add("X-RateLimit-Reset",
DateTimeOffset.UtcNow.Add(retryAfter).ToUnixTimeSeconds().ToString());
}
context.HttpContext.Response.Headers.Add("X-RateLimit-Limit", "100");
context.HttpContext.Response.Headers.Add("X-RateLimit-Remaining", "0");
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = "Too many requests. Please slow down.",
strategy = "sliding_window",
retry_after_seconds = (int?)retryAfter?.TotalSeconds ?? 60,
documentation = "https://api.example.com/docs/rate-limits"
}, ct);
};
});
var app = builder.Build();
app.UseRateLimiter();
// Public endpoints: Sliding Window
var publicApi = app.MapGroup("/api/public")
.RequireRateLimiting("public-api");
publicApi.MapGet("/products", () => new[]
{
new { Id = 1, Name = "Laptop", Price = 1200 },
new { Id = 2, Name = "Mouse", Price = 25 }
});
publicApi.MapGet("/products/{id:int}", (int id) =>
new { Id = id, Name = "Product", Price = 100 });
// Authenticated endpoints: Higher limits
var authApi = app.MapGroup("/api")
.RequireAuthorization()
.RequireRateLimiting("authenticated");
authApi.MapGet("/orders", () => new[]
{
new { Id = 1, Total = 150 }
});
authApi.MapPost("/orders", (Order order) =>
Results.Created($"/api/orders/{order.Id}", order));
// Per-user limits
app.MapGet("/api/user/profile", () => new { Name = "John Doe" })
.RequireAuthorization()
.RequireRateLimiting("per-user");
app.Run();
record Order(int Id, decimal Total);