Fixed Window vs Sliding Window vs Token Bucket in ASP.NET Core: Which Rate Limiting Algorithm Should Your .NET Team Use?

Search for a command to run...

No comments yet. Be the first to comment.
The first RAG system I shipped answered beautifully about concepts and failed completely on part numbers. Ask it "how do I reset a stuck deployment" and it nailed the answer. Ask it "what does error P

AutoMapper has been the default object mapper in .NET for over a decade, and for most of that time nobody thought about it. Version 15.0.0 changed that. It ships under a dual license now, with a free

You built the pipeline. Documents are chunked, embedded, and sitting in a vector store. Retrieval returns results. And your RAG answers still hallucinate in .NET, confidently telling users about a stu

You upgraded a working API to .NET 10, hit build, and Swashbuckle fell apart. Missing namespaces, OpenApiSchema refusing to compile, AddSecurityRequirement complaining about a delegate it never wanted

You add an entity, run dotnet ef migrations add AddOrders, and the tooling stops dead: Unable to create an object of type 'AppDbContext'. For the different patterns supported at design time, see https

Coding Droplets
303 posts
Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.
From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.
Choosing the right rate limiting algorithm for your ASP.NET Core API is one of those decisions that looks straightforward on the surface but carries real consequences in production. Since .NET 7, ASP.NET Core ships with four built-in rate limiting algorithms out of the box - Fixed Window, Sliding Window, Token Bucket, and Concurrency Limiter - and teams that pick the wrong one end up with either burst traffic spikes that overwhelm downstream services or restrictive policies that degrade legitimate user experience.
This article compares all four ASP.NET Core rate limiting algorithms side by side, walks through their mechanics, trade-offs, and real-world failure modes, and gives you a clear recommendation framework for choosing the right one for your .NET team.
The algorithm you pick shapes how bursty traffic feels to real users. The full setup with all four limiters is on Patreon. Chapter 10 of the ASP.NET Core Web API: Zero to Production course builds rate limiting and resilience into one API.
ASP.NET Core's built-in rate limiting middleware, introduced in .NET 7 via the Microsoft.AspNetCore.RateLimiting namespace, operates on the concept of limiters and policies. Each limiter is backed by a System.Threading.RateLimiting primitive. These are in-process rate limiters - they work per application instance. For distributed deployments, you need a distributed backing store (Redis is the standard choice), but the algorithm decision still applies.
The middleware integrates at the pipeline level, evaluates each request against a named policy, and returns HTTP 429 with optional Retry-After headers when limits are exceeded. You can apply policies globally, per endpoint, or per controller.
The important thing before comparing algorithms is understanding what you are trying to protect:
The Fixed Window algorithm divides time into discrete, non-overlapping windows of a fixed duration. A counter tracks requests in the current window. When the counter hits the limit, subsequent requests are rejected until the window resets.
How it works: If the window is 60 seconds and the limit is 100 requests, a client can send 100 requests in the first second and 100 more one second later when the window resets. This is the infamous boundary spike problem - two back-to-back windows can produce 2× the intended limit at the reset boundary.
Strengths:
Retry-After headersWeaknesses:
When to use Fixed Window:
The Sliding Window algorithm addresses the boundary spike problem by breaking each window into segments and tracking request counts per segment. As time advances, the oldest segment drops off and the window slides forward. This creates a rolling average effect.
How it works: With a 60-second window divided into 6 segments of 10 seconds each, the allowed count is the sum of requests across all active segments. When a new segment becomes active, the oldest expires. This means the effective limit is computed over a truly rolling 60-second period, not a discrete boundary.
Strengths:
Weaknesses:
When to use Sliding Window:
The Token Bucket algorithm works differently from window-based approaches. A bucket holds tokens up to a configured capacity. Tokens are added at a fixed replenishment rate. Each request consumes one (or more) tokens. If the bucket is empty, the request is rejected.
How it works: Configure a bucket with capacity 20 and replenishment of 10 tokens every 10 seconds. A client that has been idle accumulates up to 20 tokens and can burst 20 requests immediately. After the burst, they must wait for replenishment at 1 token/second. This explicitly allows bursting - a key distinction from window-based limiters.
Strengths:
Weaknesses:
When to use Token Bucket:
The Concurrency Limiter is fundamentally different from the other three. Instead of counting requests over a time window, it limits the number of simultaneous in-flight requests. It does not care about time - only about how many requests are being processed concurrently at any moment.
How it works: Set a permit count of 10. If 10 requests are being processed simultaneously, the 11th waits in a queue (up to a configured queue limit) or is rejected immediately. When one of the 10 completes, a queued request is processed.
Strengths:
Weaknesses:
When to use Concurrency Limiter:
| Dimension | Fixed Window | Sliding Window | Token Bucket | Concurrency Limiter |
|---|---|---|---|---|
| Measures | Requests per window | Requests per rolling window | Token consumption rate | Concurrent requests |
| Burst behavior | Allowed at boundary | Smoothed | Explicitly allowed | N/A (not time-based) |
| Memory per partition | Very low | Low - Medium | Low | Very low |
| Complexity | Low | Medium | Medium | Low |
| Best for | Daily/hourly quotas | Per-user API quotas | General API limiting | Resource protection |
| Communicating limits | Easy (window reset) | Moderate | Complex | N/A |
| Handles slow requests | No | No | No | Yes |
Most production APIs need two limiters in combination:
A typical pattern for an enterprise ASP.NET Core API:
This layered approach means each limiter handles what it is best suited for, rather than trying to use one algorithm for all scenarios.
Use this framework to make the call for your specific scenario:
Start with Token Bucket as your default. It handles the widest variety of real-world API traffic patterns. It allows legitimate burst behavior, provides a steady throughput ceiling, and is the recommendation from Microsoft's own rate limiting documentation for general-purpose API limiting. The existing GitHub repo https://github.com/codingdroplets/dotnet-rate-limiting-api demonstrates Token Bucket applied in a real API project.
Switch to Sliding Window when: your API serves external developers who game window boundaries, you need accurate "X requests per rolling minute" semantics, or you are seeing unexpected bursts at window resets in a Fixed Window implementation.
Use Fixed Window when: you are implementing daily or hourly quotas where the boundary spike is acceptable (or even desirable - it lets clients "reset" on a predictable schedule), or when simplicity and debuggability are more important than precision.
Add a Concurrency Limiter when: your endpoints have variable execution time, you have limited downstream connection pools, or you are seeing thread pool exhaustion under load. This should almost always be added as a second limiter alongside your throughput limiter, not as a replacement.
The official Microsoft documentation (learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) explains each algorithm competently but avoids giving teams a clear default recommendation. In practice, most teams would be best served by starting with Token Bucket for general endpoints and layering in a Concurrency Limiter for resource-heavy paths.
There are also important operational gaps the docs don't cover:
RedisRateLimiting package or a custom IRateLimiterPolicy).QueueLimit that queues excess requests rather than immediately rejecting them. A deep queue can delay responses significantly; a shallow queue rejects quickly. Tune this based on your latency SLA, not just throughput.For external authority on rate limiting algorithm design, the IETF's HTTP RateLimit headers specification (RFC 9380) provides a standardized way to communicate remaining quota and reset times to API consumers.
For most .NET teams building production APIs in 2026:
Fixed Window divides time into discrete, non-overlapping windows. When a window expires, the counter resets entirely. This means a client can send double the limit by timing requests across a window boundary. Sliding Window divides each window into segments and tracks a rolling count, eliminating the boundary spike problem. Sliding Window is more accurate but uses more memory per partition.
Use Token Bucket when your clients have legitimate burst patterns - mobile apps checking for updates, users exporting data, or any scenario where occasional spikes are expected. Token Bucket explicitly accumulates capacity during idle periods and allows it to be spent in a burst. Sliding Window smooths all traffic uniformly and does not differentiate between steady traffic and deliberate bursts.
No. The built-in ASP.NET Core rate limiting algorithms are all in-process. Each instance maintains its own counters, so in a load-balanced deployment with N instances, a client effectively gets N times the configured limit. For distributed rate limiting, you need a shared backing store such as Redis with a compatible IRateLimiterPolicy implementation.
ASP.NET Core returns HTTP 429 (Too Many Requests) by default when a request is rejected by a rate limiter. You can customize the rejection response, including adding a Retry-After header, using the OnRejected callback on the rate limiting policy. For Fixed Window and Sliding Window, the reset time can be calculated from the window start; for Token Bucket, it depends on token replenishment timing.
Yes. You can chain multiple rate limiting policies by using chained policies or by applying middleware that calls multiple limiters. A common pattern is to apply a global Concurrency Limiter at the pipeline level and a per-user Token Bucket limiter via the [EnableRateLimiting] attribute on specific endpoints or controllers.
No. The Concurrency Limiter protects against having too many simultaneous requests in flight, not too many requests over time. A burst of very fast requests will all pass through a Concurrency Limiter if concurrency permits it, even if the total volume is high. Use a Concurrency Limiter alongside a throughput limiter (Fixed Window, Sliding Window, or Token Bucket), not as a replacement for one.
Queued requests are held in memory and are lost if the application restarts or if the request times out before a permit becomes available. ASP.NET Core's built-in rate limiters do not persist queue state. In high-availability scenarios, design your clients to handle 429 responses and implement retry logic with exponential backoff rather than relying on server-side queuing.
Celin Daniel is Co-founder of Coding Droplets with 13+ years of hands-on experience building, shipping, and operating .NET and ASP.NET Core systems in production. The guidance here comes from real projects and production incidents, not theory.