A single client, meaning no harm at all, can take down a shared resource just as effectively as an attacker — rate limiting is the guardrail either way.
Imagine your API's /api/search endpoint calls a paid, rate-limited third-party mapping service on every request. One day, a single legitimate client — no malicious intent whatsoever, just a bug in their retry logic — starts firing thousands of requests a second. Your API happily accepts every one of them and forwards each to the mapping service, which promptly rate-limits you, breaks search for every other customer, and hands you a surprise bill. Nothing about this required an attacker. A perfectly well-behaved client with a bug was enough.
Now widen the picture: a genuine attacker deliberately hammering a login endpoint to brute-force passwords, or a script scraping your entire product catalog in seconds, or simply far more traffic from one client than your database can comfortably serve while still being fair to everyone else. All of these are the same underlying problem, wearing different hats: too many requests, from one source, in too little time. Rate limiting is the mechanism that puts a boundary on that.
In this lesson: why APIs need rate limiting, ASP.NET Core's built-in Microsoft.AspNetCore.RateLimiting middleware (part of the framework since .NET 7), its four algorithms — Fixed Window, Sliding Window, Token Bucket, and the Concurrency Limiter — with their genuine trade-offs, and how to configure and apply a policy to real endpoints.
Rate limiting is capping how many requests a client is allowed to make in a given period of time (or how many it can have in flight at once), and rejecting — usually with an HTTP 429 Too Many Requests — whatever exceeds that cap.
ASP.NET Core ships a built-in rate limiting middleware, Microsoft.AspNetCore.RateLimiting, available since .NET 7. It's configured with one or more named rate limiter policies — each backed by one of four built-in algorithms (Fixed Window, Sliding Window, Token Bucket, or Concurrency) — registered once via AddRateLimiter(), then applied to specific endpoints or endpoint groups. A request that violates its policy's limit is rejected before reaching your endpoint logic at all, with a configurable response (429 by default).
An API without any rate limiting treats every client as if it could send unlimited traffic without consequence. In practice, that assumption breaks down in three genuinely distinct ways:
What's needed is a way to cap how much traffic any single client can generate — configurable per endpoint, since a login endpoint and a static content endpoint have very different reasonable limits — enforced consistently and cheaply, before that traffic ever reaches the expensive parts of the system.
ASP.NET Core's built-in rate limiting middleware, offering four different algorithms because "cap the traffic" is not actually one single problem — a login endpoint's needs (protect against brute-force bursts) are genuinely different from a background sync endpoint's needs (allow occasional bursts, enforce a steady average), and the four algorithms below exist specifically to match those different shapes of need.
Registering a fixed window policy and applying it to one endpoint:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 10; // up to 10 requests...
opt.Window = TimeSpan.FromSeconds(30); // ...per 30-second window
opt.QueueLimit = 0; // requests over the limit are rejected immediately, not queued
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter(); // add the middleware to the pipeline
app.MapGet("/api/weather", () => Results.Ok(GetWeather()))
.RequireRateLimiting("fixed"); // apply the named policy to this endpoint
Meaning: The policy is defined once, by name, and attached to whichever endpoints need it via RequireRateLimiting("fixed") — the same reusable-policy shape you've already seen for authorization policies, applied here to traffic instead of permissions.
Different endpoints, genuinely different rate-limiting needs — matching each algorithm to the shape of the problem it actually solves:
builder.Services.AddRateLimiter(options =>
{
// Login: protect against brute-force attempts, per client IP — a strict, simple cap is appropriate here.
options.AddFixedWindowLimiter("login", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
});
// General API traffic: allow normal bursts of activity (a user rapidly paging through results)
// while still holding everyone to a fair, steady average rate.
options.AddTokenBucketLimiter("api", opt =>
{
opt.TokenLimit = 20; // bucket capacity — allows a burst of 20
opt.TokensPerPeriod = 5;
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10); // refills 5 tokens every 10 seconds
});
// Report generation: expensive, resource-heavy work — the concern isn't request volume over time,
// it's how many of these can genuinely run AT ONCE without overwhelming the reporting engine.
options.AddConcurrencyLimiter("reports", opt =>
{
opt.PermitLimit = 3; // at most 3 report generations in flight simultaneously, from anyone
opt.QueueLimit = 10; // additional requests wait in a queue rather than being rejected outright
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync("Too many requests — please slow down.", token);
};
});
app.UseRateLimiter();
app.MapPost("/api/login", LoginHandler).RequireRateLimiting("login");
app.MapGet("/api/products", GetProducts).RequireRateLimiting("api");
app.MapPost("/api/reports/generate", GenerateReport).RequireRateLimiting("reports");
Three different policies, three different algorithms, because each endpoint's actual risk is genuinely different: login needs strict, simple abuse protection (Fixed Window); everyday API browsing needs to tolerate normal bursty usage while staying fair overall (Token Bucket); expensive report generation needs to cap simultaneous load on a shared resource, regardless of how spread out over time the requests are (Concurrency Limiter).
A fixed window is like a ride that only counts riders per exact clock hour — if 50 people rush in during the last minute of the 2 PM hour, and another 50 the instant 3 PM starts, the ride was briefly swamped with 100 people in about two minutes, even though "50 per hour" was the rule. A sliding window smooths that by tracking riders on a continuously moving hour, not a clock-aligned one, so no single sixty-second stretch can be double-counted against two separate hours.
A token bucket is like a ride that hands out ride tickets that trickle in steadily over the day, but lets you save up a few and use them all at once if you haven't ridden in a while — some real burstiness is fine, as long as your average pace over the day stays within bounds. A concurrency limiter isn't about tickets over time at all — it's the ride physically only having 3 cars, so however many people show up over the whole day, never more than 3 groups are ever mid-ride at the same instant.
AddPolicy (the more general registration method, alongside the algorithm-specific shortcuts shown above) accepts a partition key — commonly the client's IP address, or an authenticated user's ID (read from the ClaimsPrincipal the authentication lesson covered) — so each distinct client gets its own independent counter, rather than everyone sharing one global limit.It's tempting to think of rate limiting purely as a security feature against malicious traffic. As the hook illustrated, entirely well-intentioned clients can just as easily overwhelm a shared resource through a bug, an unexpectedly popular feature, or simple scale. Rate limiting protects the system's overall health and fairness, independent of anyone's intent.
A time-based limit (fixed window, sliding window, token bucket) asks "how many requests happened in this period?" A concurrency limit asks "how many requests are actively in progress right now?" A client sending requests one-at-a-time, waiting for each to finish before sending the next, could never trip a concurrency limiter no matter how many total requests it sends over a day — but it could easily trip a strict fixed-window limit if it sends them fast enough within one window. These are answering different questions, and picking the wrong one for a given problem (protecting a limited connection pool vs. protecting against a chatty client) won't actually solve it.
A single fixed-window policy with no partition key, meaning one aggressive client can exhaust the entire limit and lock out every other legitimate client sharing it.
Partition the limiter by client IP or authenticated user ID, so each caller gets its own independent budget.
Protecting a genuinely fragile downstream resource (a strict third-party rate limit) with Fixed Window, not accounting for the possibility of nearly double the configured rate landing right at a window boundary.
Use Sliding Window (or a sufficiently conservative Token Bucket configuration) when the boundary-burst behavior would actually cause a real problem for what's being protected.
Using a Token Bucket policy to try to cap how many report generations can run at once, when the actual risk is overwhelming the reporting engine with too much concurrent work — a slow, spread-out trickle of requests could still pass a time-based limit while still overloading the engine if enough of them land at once.
Use the Concurrency Limiter specifically when the concern is simultaneous in-flight load on a resource, not total volume over a time window.
Microsoft.AspNetCore.RateLimiting middleware (since .NET 7) caps traffic per named policy, applied to specific endpoints via RequireRateLimiting().You've seen why APIs need rate limiting and how each of the four built-in algorithms handles the problem differently. Let's check your understanding.
1. A fixed window limiter allows 100 requests per 60-second window. What genuine weakness does this algorithm have?
Correct: B
Why B is correct: Because the window resets entirely at fixed clock-aligned boundaries, a client can exploit both the tail end of one window and the start of the next, briefly seeing close to double the configured rate.
Why A is incorrect: Fixed window limiters, like all the built-in algorithms, are applied per named policy to specific endpoints via RequireRateLimiting().
Why C is incorrect: The built-in middleware tracks state in memory, not in a database, for its counters.
Why D is incorrect: Rate limiting middleware actively rejects requests over the limit (typically with 429), it doesn't just log and allow them through.
Reinforcement: The boundary-burst weakness is the specific, well-known trade-off Fixed Window makes for its simplicity — and it's exactly what Sliding Window exists to fix.
2. Which algorithm specifically limits how many requests can be actively processed at the same moment, rather than how many occur over a period of time?
Correct: D
Why D is correct: The Concurrency Limiter caps in-flight, simultaneous requests — a request holds a slot from when it starts until it finishes, independent of how request volume is distributed over time.
Why A, B, and C are incorrect: Fixed Window, Sliding Window, and Token Bucket are all fundamentally time-based — they count requests over a period, which is a genuinely different measurement than "how many are in flight right now."
Reinforcement: Concurrency limiting answers "how much is happening at once," a distinct question from every time-window-based algorithm.
3. You're rate limiting a general-purpose API endpoint where you want to allow users to quickly page through several pages of results in a row (a natural short burst), while still enforcing a fair average request rate over time. Which algorithm best fits this need?
Correct: B
Why B is correct: This is precisely the shape of problem Token Bucket is designed for — a full bucket allows a short burst of paging requests, and the steady token refill rate keeps the long-run average fair.
Why A is incorrect: Fixed Window can technically allow some burstiness too, but it's not designed for it deliberately — and it comes with the specific boundary-burst weakness rather than a controlled, intentional one.
Why C is incorrect: Paging through results one request at a time isn't primarily a concurrency concern (multiple requests in flight simultaneously) — it's a request-volume-over-time concern, which is what Token Bucket addresses.
Why D is incorrect: This is exactly what Token Bucket, a built-in algorithm, is designed to express — no custom solution is needed.
Reinforcement: "Allow some burst, enforce a steady average" is Token Bucket's defining characteristic — recognize that phrasing and reach for it directly.
4. Why is it a mistake to apply a single, unpartitioned rate limit policy shared by every client hitting an endpoint?
Correct: B
Why B is correct: Without a partition key (like client IP or user ID), all callers share one counter — so a single high-traffic client can exhaust the whole limit and lock out everyone else, directly undermining the "fairness" goal rate limiting is meant to serve.
Why A is incorrect: Unpartitioned (single, shared) policies are valid configuration — the earlier Simple Example uses exactly one — they're just usually the wrong choice for multi-client scenarios.
Why C is incorrect: Partitioning by key is a general capability available across the rate limiting system, not specific to one algorithm.
Why D is incorrect: This is a real fairness problem, explicitly called out as a common mistake, not merely a performance concern.
Reinforcement: Partitioning by client is usually essential in real multi-tenant APIs — a shared limit with no partition key defeats the fairness goal that motivated rate limiting in the first place.
You now know how to protect your API — and everything behind it — from excessive traffic, whether it comes from an attacker, a bug, or simply more legitimate demand than one client should fairly consume.
dotnetmadeeasy.com — Learn C# and .NET, the right way.