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

Sliding Window Rate Limiting Strategy

Table of Contents

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

  1. Prevents boundary bursts - Smooth traffic distribution
  2. Production-ready - Used by major APIs (Twitter, GitHub)
  3. Good performance - Only slightly slower than Fixed Window
  4. Easy upgrade - Drop-in replacement for Fixed Window

Related Articles

  1. Previous: Fixed Window Strategy
  2. Next: Token Bucket Strategy
  3. 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:

  1. Window: 60 seconds
  2. Segments: 6 (10 seconds each)
  3. Limit: 15 requests/minute
  4. 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);
Share this lesson: