# Add Rate Limiting to an ASP.NET Core API

Yes. An agent adds the built-in rate limiter middleware to your ASP.NET Core API, a strict policy on login and registration, tests for the 429 path, and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-rate-limiting/aspnet-core/), in a Project that has your repository.

## What the agent does

The agent starts by reading how requests enter the app and how errors leave it, because a rate limiter that answers in a different shape from every other error breaks clients. In an API like `realworld-apps/aspnetcore-realworld-example-app`, `global.json` pins SDK `10.0.402`, `Directory.Build.props` targets `net10.0` with `TreatWarningsAsErrors` and the recommended analyzers on, and `Directory.Packages.props` manages every package version centrally with lock files enforced on restore. `src/Conduit/Program.cs` adds controllers under the `api` prefix through `ApiRoutePrefixConvention`, puts `ErrorHandlingMiddleware` first, then `UseRouting`, `UseCors`, `UseAuthentication` and `UseAuthorization`. `src/Conduit/Infrastructure/Errors/ErrorHandlingMiddleware.cs` writes every error as JSON with an `errors` object, which the integration tests assert on.

The routes that need the strict policy are in `src/Conduit/Features/Users/UsersController.cs`: `POST users` registers and `POST users/login` signs in, both anonymous, and `Login.cs` answers a wrong password with a 401 through `RestException`. `JwtIssuerOptions.cs` makes tokens valid for five minutes by default, so clients call login often, which argues for a per-address limit rather than a tiny one. Because the rate limiter lives in the shared framework as `Microsoft.AspNetCore.RateLimiting`, no package is added, `Directory.Packages.props` and the `packages.lock.json` files stay unchanged, and the locked restore in CI keeps passing.

From that the agent writes a `RateLimitingExtensions` method beside `ServicesExtensions.cs` that calls `AddRateLimiter` with two policies bound from configuration: `auth`, a fixed window keyed by client address for the two `UsersController` actions, and `write`, a sliding window keyed by the `sub` claim when the request is authenticated and by address otherwise, for the article, comment, follow and favorite endpoints. `RejectionStatusCode` is 429 and `OnRejected` writes the same `{"errors": ...}` body the error middleware uses and a `Retry-After` header from the lease's metadata. `app.UseRateLimiter()` goes after `UseAuthentication` so the user partition sees the claims, and `[EnableRateLimiting("auth")]` goes on the two actions. The limits read from a `RateLimiting` configuration section so `ApiFactory` and the spec suites can set them, with defaults the pull request states.

Then it proves the change the way the repository proves everything. It adds an xUnit test in `tests/Conduit.IntegrationTests/` that uses `ApiFactory` with `UseSetting` to make the auth limit small, posts to `/api/users/login` until the window is spent, and asserts 429, the `errors` body and the `Retry-After` header, plus a test that the limit resets after the window. It runs `dotnet run --project build/build.csproj -- test`, which formats with CSharpier, builds in Release with warnings as errors and runs the test projects, then runs `make test-hurl-with-managed-server` so the RealWorld spec suite, which fires many requests in a few seconds, confirms the default limits do not trip on ordinary use.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add rate limiting to this ASP.NET Core API. Read global.json, Directory.Build.props, Directory.Packages.props, src/Conduit/Program.cs, src/Conduit/ServicesExtensions.cs, src/Conduit/Infrastructure/Errors/ErrorHandlingMiddleware.cs, src/Conduit/Features/Users/UsersController.cs, Login.cs, src/Conduit/Infrastructure/Security/JwtIssuerOptions.cs, tests/Conduit.IntegrationTests/ApiFactory.cs and ApiTests.cs, the Makefile and AGENTS.md first. Use the framework's Microsoft.AspNetCore.RateLimiting middleware and add no package. Define an auth policy, a fixed window partitioned by client address, on POST users and POST users/login, and a write policy, a sliding window partitioned by the sub claim for authenticated requests and by address otherwise, on the endpoints that create or change data. Reject with 429, the same errors JSON shape the error middleware writes, and a Retry-After header. Read the limits from a RateLimiting configuration section with sensible defaults. Register the middleware after authentication. Add integration tests that lower the limit through ApiFactory settings and assert the 429 response and the reset. Run dotnet run --project build/build.csproj -- test and make test-hurl-with-managed-server in the sandbox, fix anything until both pass, and open a pull request that lists the policies, their defaults and how to change them.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch with the new extension method, the `[EnableRateLimiting]` attributes on the two user actions and the write endpoints, the `UseRateLimiter` line in `Program.cs`, and the new test file. The description lists both policies with their partition keys, their default limits and windows, the configuration keys that change them, and why the middleware sits after authentication. It names the files it read to decide the error shape and the route list.

The transcript on the session keeps every command with its output: the build target with CSharpier, the Release build with warnings as errors, each test run including the one that produced the 429, and the Hurl suite against the managed server. The repository's own `Build and Test` and `RealWorld API Tests` workflows run on the pull request too, so the locked restore, the format check and both spec suites report beside the diff.

Before merging, a reviewer checks that the `auth` limit is strict enough to slow a password guess but wide enough for a client that refreshes a five-minute token, that the address partition reads the forwarded header when the API sits behind a proxy, that the `errors` body matches what the front end already parses, and that the spec suites passed with the defaults rather than with raised limits. If production runs more than one instance, one follow-up message to the same session moves the counters to a shared store.

## Run it on every Linear issue labelled rate-limit

A team that tracks hardening work in Linear starts one run per issue from an automation with a `linear` trigger on the `rate-limit` label. The automation adds the issue after this prompt, so the agent reads which endpoints and limits the issue asks for and starts in the repository the automation names.

Each run opens its own pull request from its own sandbox, and an automation run has its own `max_runs_per_hour` cap. The pull request links back to the issue, and the transcript holds the build target and spec suite output for that change.

## Why run it on Rush?

The work here is not the middleware registration, it is the proof around it: a build with warnings as errors and a formatter that rejects the smallest style slip, an integration test that has to spend a window without flaking, and two spec suites that send requests fast enough to trip a careless default. Running all of that takes a machine for a while. In a Rush sandbox the agent has its own .NET SDK from `global.json`, its own SQLite databases and its own ports, and the sandbox parks when the turn ends.

The pull request carries its own proof. The 429 response with its `Retry-After` header, the formatted build and the Hurl run are in the transcript beside the commands that produced them, so a reviewer can see the limiter rejects a flood and stays out of the way of the spec suite, rather than taking the description's word for it.

## Frequently asked questions

### Does it add a NuGet package?

No. The rate limiter middleware ships in the shared framework as `Microsoft.AspNetCore.RateLimiting`, so `Directory.Packages.props` and every `packages.lock.json` stay as they are and the locked restore in CI passes without a lock update.

### Will the RealWorld spec tests still pass?

They have to. The agent runs `make test-hurl-with-managed-server` after the change, and the Bruno variant on request, so the default limits are checked against a suite that sends many requests in a few seconds. If a default trips it, the agent raises the default rather than special-casing the tests, and says so.

### What does a rejected request look like?

A 429 with a JSON body in the same `errors` shape `ErrorHandlingMiddleware` writes for every other error, and a `Retry-After` header computed from the window. The integration test asserts all three, so a client that already parses the error body needs no change.

### Does it work with several instances behind a load balancer?

The policies keep their counters in process, which is correct for one instance and per-instance for several. The pull request says so and names the configuration section to tune. Moving the counters to a shared store is a follow-up message to the same session, since it changes the deployment rather than the endpoints.
