All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.

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.