Asp.Net Core Security Rate Limit Created: 24 Jan 2026 Updated: 24 Jan 2026

Fixed Window Rate Limiting Strategy

Table of Contents

  1. Introduction
  2. How It Works
  3. Implementation
  4. Mathematical Analysis
  5. The Boundary Problem
  6. Production Considerations
  7. When to Use
  8. 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?

  1. Easiest to understand - Simple counting logic
  2. Minimal memory - One counter per window
  3. Fast performance - O(1) lookup time
  4. Perfect for learning - Great first rate limiter

Related Articles

  1. Previous: Overview
  2. Next: Sliding Window Strategy
  3. 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();
Share this lesson: