Asp.Net Core Security
Rate Limit
Created: 24 Jan 2026
Updated: 24 Jan 2026
Fixed Window Rate Limiting Strategy
Table of Contents
- Introduction
- How It Works
- Implementation
- Mathematical Analysis
- The Boundary Problem
- Production Considerations
- When to Use
- Complete Example
Introduction
Fixed Window is the simplest rate limiting strategy. Think of it as dividing time into buckets—each bucket gets a fixed quota of requests. When the bucket is full, no more requests are allowed until the next time window starts.
Quick Example:
Rule: 10 requests per minute
Timeline:
00:00-00:59 → Allow 10 requests ✓
01:00-01:59 → Counter resets, allow 10 more ✓
02:00-02:59 → Counter resets again
Why Start Here?
- ✅ Easiest to understand - Simple counting logic
- ✅ Minimal memory - One counter per window
- ✅ Fast performance - O(1) lookup time
- ✅ Perfect for learning - Great first rate limiter
Related Articles
- Previous: Overview
- Next: Sliding Window Strategy
- Compare: Strategy Comparison Guide
How It Works
Core Algorithm
For each incoming request at time T:
1. Calculate current window: window_id = floor(T / window_size)
2. Check counter: if count[window_id] < limit:
- Increment counter
- Allow request ✓
else:
- Reject request ❌ (429 Too Many Requests)
Visual Representation
Time Windows (1 minute each):
Window 1: [00:00:00 - 00:00:59]
Limit: 10 requests
┌─────────────────────────────────────────────────────┐
│ Request: 1 2 3 4 5 6 7 8 9 10 │
│ Time (s): 5 10 15 20 25 30 35 40 45 50 │
│ Status: ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ │
└─────────────────────────────────────────────────────┘
Request 11 at 55s → ❌ REJECTED (quota exhausted)
Window 2: [00:01:00 - 00:01:59]
Counter resets to 0 at exactly 01:00
┌─────────────────────────────────────────────────────┐
│ Request: 1 2 3 4 5 6 7 8 9 10 │
│ Time (s): 2 5 8 12 18 22 28 35 42 50 │
│ Status: ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ │
└─────────────────────────────────────────────────────┘
State Machine
┌─────────────┐
│ Request │
│ Arrives │
└──────┬──────┘
│
v
┌──────────────────────────────────┐
│ Calculate Window ID │
│ window_id = floor(time / 60) │
└──────┬───────────────────────────┘
│
v
┌──────────────────────────────────┐
│ Check Counter │
│ count[window_id] < limit? │
└──────┬───────────────────────────┘
│
┌───┴───┐
│ │
YES NO
│ │
v v
┌─────┐ ┌─────┐
│Allow│ │Reject│
│ 200 │ │ 429 │
└─────┘ └─────┘
│ │
v │
┌─────────┤
│count++ │
└─────────┘
Implementation
Complete C# Implementation
using Microsoft.AspNetCore.RateLimiting;
namespace SecurityApp.API.RateLimiting;
/// <summary>
/// Fixed Window Rate Limiter
/// Simplest rate limiting strategy using fixed time windows.
/// </summary>
public static class FixedWindowRateLimiter
{
public const string PolicyName = "FixedWindow";
/// <summary>
/// Configuration:
/// - Window: 1 minute (fixed 60-second intervals)
/// - PermitLimit: 10 requests per window
/// - QueueLimit: 2 requests can wait in queue
///
/// Example Timeline:
/// [00:00-00:59] → 10 requests allowed
/// [01:00-01:59] → Counter resets, 10 new requests allowed
/// </summary>
public static void Configure(RateLimiterOptions rateLimiterOptions)
{
rateLimiterOptions.AddFixedWindowLimiter(PolicyName, options =>
{
options.PermitLimit = 10;
options.Window = TimeSpan.FromMinutes(1);
options.QueueProcessingOrder = System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst;
options.QueueLimit = 2;
});
}
}
Registration in Program.cs
var builder = WebApplication.CreateBuilder(args);
// Configure rate limiting
builder.Services.AddRateLimiter(options =>
{
// Register Fixed Window strategy
FixedWindowRateLimiter.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 = "Too many requests. Please wait before retrying.",
retry_after_seconds = (int)retryAfter.TotalSeconds
}, cancellationToken);
};
});
var app = builder.Build();
// Enable rate limiting middleware
app.UseRateLimiter();
// Apply to endpoints
app.MapGet("/api/data", () => new { data = "Hello World" })
.RequireRateLimiting(FixedWindowRateLimiter.PolicyName)
.WithName("GetData");
app.Run();
Applying to Specific Endpoints
// Option 1: Per-endpoint
app.MapGet("/api/public", () => Results.Ok("Public data"))
.RequireRateLimiting("FixedWindow");
// Option 2: Group of endpoints
var rateLimitedGroup = app.MapGroup("/api")
.RequireRateLimiting("FixedWindow");
rateLimitedGroup.MapGet("/users", () => Results.Ok(new[] { "User1", "User2" }));
rateLimitedGroup.MapGet("/products", () => Results.Ok(new[] { "Product1", "Product2" }));
// Option 3: Global (all endpoints)
app.UseRateLimiter(); // Already covers all endpoints by default
Mathematical Analysis
Request Distribution
Scenario: 10 requests/minute limit
Best Case (Evenly Distributed)
Requests arrive every 6 seconds:
00s → Request 1 ✓
06s → Request 2 ✓
12s → Request 3 ✓
18s → Request 4 ✓
24s → Request 5 ✓
30s → Request 6 ✓
36s → Request 7 ✓
42s → Request 8 ✓
48s → Request 9 ✓
54s → Request 10 ✓
60s → Window resets
Result: Perfect distribution, 1 request per 6 seconds
Worst Case (All at Once)
All 10 requests arrive at 0s:
00s → Requests 1-10 ✓ (all accepted)
01s → Request 11 ❌ (rejected)
...
59s → Request N ❌ (rejected)
60s → Window resets, requests allowed again
Result: 10 requests in 1 second, then 59 seconds of rejections
Throughput Calculation
Configuration:
- Limit: 10 requests
- Window: 60 seconds
Effective Rate:
Average = Limit / Window = 10 / 60 = 0.167 requests/second
Per Minute: 10 requests/minute
Per Hour: 10 × 60 = 600 requests/hour
Per Day: 600 × 24 = 14,400 requests/day
Memory Footprint
Per Window State:
- Window ID: 8 bytes (long)
- Counter: 4 bytes (int)
- Timestamp: 8 bytes (DateTime)
Total: ~20 bytes per window
For 1000 concurrent users:
- Memory: 1000 × 20 bytes = 20 KB
- Very lightweight!
The Boundary Problem
Understanding the Issue
The boundary problem is Fixed Window's biggest flaw: traffic bursts at window boundaries.
Problem Visualization
Window 1: [00:59:00 - 00:59:59]
Limit: 10 requests
00:59:50 → 10 requests arrive (all accepted) ✓
00:59:59 → Window ends
Window 2: [01:00:00 - 01:00:59]
01:00:00 → Counter resets to 0
01:00:01 → 10 requests arrive (all accepted) ✓
Result: 20 requests in 11 seconds!
This is 2x the intended rate!
Real-World Impact
Scenario: E-commerce API
Limit: 100 requests/minute (to protect database)
Attack:
- Attacker sends 100 requests at 59.5 seconds
- Attacker sends 100 requests at 0.5 seconds
- Total: 200 requests in 1 second
- Database: Overloaded! 💥
Expected: 100 requests/minute = 1.67 req/s
Actual: 200 requests/second (120x over!)
Mitigation Strategies
1. Shorter Windows
// Instead of 1 minute window
options.Window = TimeSpan.FromMinutes(1);
options.PermitLimit = 60;
// Use 10-second windows
options.Window = TimeSpan.FromSeconds(10);
options.PermitLimit = 10;
// Reduces boundary burst:
// Before: 120 requests in 2 seconds possible
// After: 20 requests in 2 seconds possible
2. Use Sliding Window
// Better solution: Switch to Sliding Window
// See: Sliding Window Strategy article
rateLimiterOptions.AddSlidingWindowLimiter(PolicyName, options =>
{
options.PermitLimit = 60;
options.Window = TimeSpan.FromMinutes(1);
options.SegmentsPerWindow = 6;
});
3. Add Queue
// Smooth bursts with queueing
options.QueueLimit = 10; // Allow 10 requests to wait
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
// Requests 1-10: Immediate
// Requests 11-20: Queued
// Requests 21+: Rejected
Production Considerations
Configuration Options
Basic Configuration
options.PermitLimit = 100; // Max requests per window
options.Window = TimeSpan.FromMinutes(1); // 60-second windows
With Queueing
options.QueueLimit = 10; // Max 10 queued requests
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; // FIFO
Multiple Strategies
builder.Services.AddRateLimiter(options =>
{
// API endpoints: Generous limit
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 1000;
opt.Window = TimeSpan.FromMinutes(1);
});
// Authentication: Strict limit (brute force protection)
options.AddFixedWindowLimiter("auth", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
});
// Admin: Very strict
options.AddFixedWindowLimiter("admin", opt =>
{
opt.PermitLimit = 10;
opt.Window = TimeSpan.FromHours(1);
});
});
// Apply different limits
app.MapGet("/api/data", ...).RequireRateLimiting("api");
app.MapPost("/api/login", ...).RequireRateLimiting("auth");
app.MapGet("/admin/users", ...).RequireRateLimiting("admin");
Response Headers
// Add standard rate limit headers
app.Use(async (context, next) =>
{
await next();
// Add headers to successful responses
if (context.Response.StatusCode == 200)
{
context.Response.Headers.Add("X-RateLimit-Limit", "100");
context.Response.Headers.Add("X-RateLimit-Remaining", "75");
context.Response.Headers.Add("X-RateLimit-Reset",
DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds().ToString());
}
});
Error Handling
options.OnRejected = async (context, ct) =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
logger.LogWarning("Rate limit exceeded for {IP} on {Path}",
context.HttpContext.Connection.RemoteIpAddress,
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();
}
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
message = "You have exceeded the rate limit. Please slow down.",
limit = 100,
window = "1 minute",
retry_after_seconds = (int?)retryAfter?.TotalSeconds ?? 60,
documentation = "https://api.example.com/docs/rate-limits"
}, ct);
};
When to Use
✅ Perfect For
1. Internal APIs
// Trusted microservices communication
app.MapGet("/internal/health", () => "OK")
.RequireRateLimiting("internal"); // Simple fixed window is fine
// Configuration
options.AddFixedWindowLimiter("internal", opt =>
{
opt.PermitLimit = 1000; // Generous limit
opt.Window = TimeSpan.FromMinutes(1);
});
2. Development/Testing
#if DEBUG
options.AddFixedWindowLimiter("dev", opt =>
{
opt.PermitLimit = 10000; // Very high for testing
opt.Window = TimeSpan.FromMinutes(1);
});
#else
// Use production strategy (Sliding Window)
#endif
3. Simple APIs
// Low-traffic admin dashboard
app.MapGet("/admin/stats", () => GetStats())
.RequireRateLimiting("admin");
// 10 requests per hour is plenty
options.AddFixedWindowLimiter("admin", opt =>
{
opt.PermitLimit = 10;
opt.Window = TimeSpan.FromHours(1);
});
4. Cost-Sensitive Scenarios
Memory Usage Comparison:
Fixed Window: 20 bytes per user
Sliding Window: 120 bytes per user (6 segments)
Token Bucket: 40 bytes per user
For 1M users:
Fixed Window: 20 MB ✓
Sliding Window: 120 MB
Token Bucket: 40 MB
Winner: Fixed Window (if boundary bursts acceptable)
❌ Avoid When
1. Public-Facing APIs
Risk: Boundary burst attacks
// DON'T USE for public API:
app.MapGet("/api/public/search", ...)
.RequireRateLimiting("FixedWindow"); // ❌ Vulnerable
// USE Sliding Window instead:
app.MapGet("/api/public/search", ...)
.RequireRateLimiting("SlidingWindow"); // ✓
2. High-Security Requirements
// Authentication endpoint - CRITICAL
// Fixed Window = Vulnerable to burst attacks
// ❌ DON'T:
app.MapPost("/api/auth/login", ...)
.RequireRateLimiting("FixedWindow");
// ✓ DO: Use Sliding Window or Chained
app.MapPost("/api/auth/login", ...)
.RequireRateLimiting("SlidingWindow")
.RequireRateLimiting("PerUser");
3. Strict Rate Enforcement Needed
Scenario: API with guaranteed SLA
SLA: "No more than 100 requests/minute"
Fixed Window: Can burst to 200 req/min at boundaries
Result: SLA violation! ❌
Solution: Use Sliding Window or Token Bucket
Complete Example
Full Production Setup
// Program.cs
using Microsoft.AspNetCore.RateLimiting;
using SecurityApp.API.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// Configure rate limiting
builder.Services.AddRateLimiter(options =>
{
// Global rate limit
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
return RateLimitPartition.GetFixedWindowLimiter(
"global",
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromMinutes(1)
});
});
// API endpoints
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 10;
});
// Authentication (stricter)
options.AddFixedWindowLimiter("auth", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 0; // No queue for auth
});
// Rejection handler
options.OnRejected = async (context, ct) =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Program>>();
logger.LogWarning(
"Rate limit exceeded: {Policy} for {IP} on {Path}",
context.HttpContext.GetEndpoint()?.Metadata
.GetMetadata<EnableRateLimitingAttribute>()?.PolicyName ?? "global",
context.HttpContext.Connection.RemoteIpAddress,
context.HttpContext.Request.Path);
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 = "Too many requests. Please slow down.",
retry_after_seconds = (int?)retryAfter?.TotalSeconds ?? 60
}, ct);
};
});
var app = builder.Build();
app.UseRateLimiter();
// Public API endpoints
app.MapGet("/api/products", () => new[]
{
new { Id = 1, Name = "Laptop" },
new { Id = 2, Name = "Mouse" }
})
.RequireRateLimiting("api")
.WithName("GetProducts");
// Authentication endpoint
app.MapPost("/api/auth/login", (LoginRequest request) =>
{
// Validate credentials
return Results.Ok(new { token = "jwt-token" });
})
.RequireRateLimiting("auth")
.WithName("Login");
app.Run();