Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
Name what is limited and where the limit lives
A rate limiter constrains admission according to a configured algorithm and ownership boundary. The external project chooses a named fixed-window policy with two permits, a one-minute window, no queue and status 429 for rejection. That is one in-process teaching policy, not a distributed tenant quota or a complete availability defense. The portable companion owns a finite two-unit work budget with no clock. It demonstrates exhaustion only and deliberately does not pretend to implement the framework's window reset or scheduling. Real timing behavior belongs to the running ASP.NET project.
Read the first three admissions on a fresh process
The console calls TryTake three times, printing accepted, accepted and rejected. Its remaining count cannot become negative because an exhausted budget rejects before decrementing. On the external server, three immediate sequential requests during the same fresh window receive 200, 200 and 429. The queue limit is zero, so the third request does not wait for a future permit. The one-minute duration makes the classroom sequence easy to complete before replenishment, but tests should still state that time boundary instead of treating the result as a permanent property of an endpoint.
Registering a policy does not apply it everywhere
AddRateLimiter registers the policy and middleware services. UseRateLimiter participates in the pipeline, while RequireRateLimiting attaches the named policy to the selected endpoint. The sample explicitly places routing before the limiter so endpoint metadata is available. A different route without that policy is not automatically covered merely because a named policy exists. A global limiter would have another application boundary. Keep these choices visible when adding endpoints, otherwise a refactor can silently leave expensive work outside the intended admission rule while registration code still appears to configure a limiter.
Shared process state is different from a distributed quota
This named limiter does not partition by authenticated account and does not coordinate across multiple server instances. Restarting the process resets its local state. An authenticated identity or trusted client partition requires an appropriate policy design; raw user-controlled keys can create their own resource and fairness problems. A queue also changes latency and cancellation behavior, which is why this first example omits one. Do not label the two-permit policy as protection against every form of abuse. Upstream infrastructure, timeouts, concurrency and application costs remain separate boundaries even when admission limiting works exactly as configured.
Practice: prove endpoint-policy application
Add an otherwise identical /unlimited endpoint without RequireRateLimiting. Exhaust /limited with the three baseline requests and then call the separate route. Predict which requests the named policy should reject. Next apply the named policy to that route and specify whether requests now consume the same intended policy budget. Use a fresh server for each independent expected sequence. The hint is to inspect actual endpoint metadata and policy ownership rather than assuming a registration line names one independent limiter per route. Keep a global limiter out of this experiment until the named policy is understood.
Practice: distinguish a window from a concurrency limit
Write a separate design for an endpoint whose problem is two expensive operations running at once rather than two admissions in a minute. Explain why the fixed-window example does not directly express that concurrent-work requirement. Build a separate concurrency-limiter project with controlled request completion signals if you want execution evidence, and test queued or rejected admission deliberately. Do not use a public slow service or arbitrary sleeps as the operation boundary. Keep the original window policy as a regression example so changing algorithms does not silently redefine the behavior clients previously observed.
Specify timing, queueing and rejection independently
A useful limiter review names the protected endpoint, algorithm, permit count, replenishment boundary, queue behavior, partition and rejection contract. The complete project makes the first classroom choices explicit, and its real requests establish the configured rejection status. The portable finite budget cannot prove time-based behavior, but remains a deterministic check of exhaustion handling. Preserve that honesty when adding more realistic policies. A client can respond differently to queued work, a 429 response or an operational failure, so avoid collapsing them into a generic unsuccessful request or claiming that one successful three-call transcript covers production resilience.
- Named registration, middleware execution and endpoint application are separate requirements.
- Window limits, concurrent-work limits, queues and distributed quotas solve different contracts.
Rate limiting and an explicit rejection policy — real ASP.NET Core 10 project
Set up the SDK project
- Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
- From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
- Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
- Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
- Request /limited three times immediately after starting a fresh server; compare status codes. Window reset requires elapsed server time, not another browser console run.
Download complete SDK project ↗
Project files
WebLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Program.cs
Download file ↗using Microsoft.AspNetCore.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options => {
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("demo", limiter => { limiter.PermitLimit = 2; limiter.Window = TimeSpan.FromMinutes(1); limiter.QueueLimit = 0; });
});
var app = builder.Build(); app.UseRouting(); app.UseRateLimiter();
app.MapGet("/limited", () => TypedResults.Ok(new { accepted = true })).RequireRateLimiting("demo"); app.Run();
Expected result
Three immediate sequential GET /limited calls on a fresh process within one window produce 200, 200, 429. No requests wait in a limiter queue.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var budget = new Budget(2); for (int i = 0; i < 3; i++) Console.WriteLine(budget.TryTake() ? "accepted" : "rejected");
sealed class Budget { private int remaining; public Budget(int count) { if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); remaining = count; } public bool TryTake() { if (remaining == 0) return false; remaining--; return true; } }
C# practice output
accepted accepted rejected
The portable budget has no time window; only the real external middleware demonstrates rate limiting.