All jobs

Add Rate Limiting to a FastAPI API

Yes. An agent adds a rate-limit dependency to the login, signup and password-reset routes of your FastAPI app, proves the 429 with pytest and opens a pull request.

Run this on RushIn a Project that has your repository.

What the agent does

The agent clones the repository into its own cloud sandbox and reads the app before choosing an approach. In a FastAPI app like fastapi/full-stack-fastapi-template the backend lives under backend/: pyproject.toml requires Python >=3.14,<4.0, fastapi[standard], sqlmodel, psycopg[binary], pyjwt, pwdlib[argon2,bcrypt] and sentry-sdk[fastapi], with pytest, mypy in strict mode, ty, ruff and coverage as the dev group, all managed by uv with a uv.lock. app/main.py builds the FastAPI app, adds CORSMiddleware for settings.FRONTEND_HOST and mounts api_router at settings.API_V1_STR. A search for any limiter finds nothing, and Settings in app/core/config.py has no Redis: the only backing service in compose.yml besides Traefik and Adminer is postgres:18.

It then picks the routes that matter. app/api/routes/login.py has POST /login/access-token, which takes an OAuth2PasswordRequestForm and answers 400 with Incorrect email or password, plus POST /password-recovery/{email} and POST /reset-password/, and app/api/routes/users.py has POST /users/signup. Those four take a credential or an email from an anonymous caller, so they are the ones a password sprayer or a mail-bomber hits. The authenticated routes behind CurrentUser in app/api/deps.py are left alone unless the prompt says otherwise.

Because there is no Redis, the agent writes the limiter as a dependency rather than adding a store: app/core/rate_limit.py with a RateLimiter class holding a sliding window per client address and route in process memory, raising HTTPException with status 429 and a Retry-After header when the window is full. Limits come from new Settings fields with defaults, so they change in .env, not in code. It wires the dependency onto the four routes with Depends, which the project's ruff config already allows in defaults by ignoring B008. One thing it reads in backend/Dockerfile shapes the pull request: the container runs fastapi run --workers 4, so an in-process window is per worker and the effective limit is up to four times the configured one. The agent states that in the description and offers Traefik's ratelimit middleware or a Redis-backed store as the follow-up for a cluster.

Tests go in tests/api/routes/test_rate_limit.py using the module-scoped client fixture from tests/conftest.py: a loop of wrong-password posts to /login/access-token that expects 400 until the window fills and 429 after, with Retry-After present, and a check that a different route is unaffected. It runs bash scripts/test.sh, which wraps pytest in coverage run, because .github/workflows/test-backend.yml ends with coverage report --fail-under=90, and then ruff check, mypy and ty, since strict typing is part of this project's CI.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Add rate limiting to the anonymous routes of this FastAPI app: POST /login/access-token, POST /password-recovery/{email}, POST /reset-password/ and POST /users/signup. Read backend/app/main.py, app/core/config.py, app/api/deps.py, the route files and backend/Dockerfile first. Implement it as a dependency in app/core/rate_limit.py with a per-client sliding window in process memory, limits read from new Settings fields with defaults, and a 429 with a Retry-After header. Do not add Redis. Write tests in tests/api/routes/test_rate_limit.py using the existing client fixture, run bash scripts/test.sh so coverage stays over the CI threshold, then ruff, mypy and ty. Open a pull request that states the per-worker caveat from the Dockerfile's --workers setting and the defaults you chose.",
  "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 that adds app/core/rate_limit.py and tests/api/routes/test_rate_limit.py, adds the limit fields to Settings in app/core/config.py, and adds one Depends to each of the four route decorators in login.py and users.py. pyproject.toml and uv.lock do not change, because nothing new is installed. The description lists the four routes, the default limits it chose and the environment variables that override them, and the paragraph about --workers 4 that explains why the limit is per process.

A reviewer checks that the window keys on the client address the app actually sees, which behind Traefik is the forwarded address only if the server is started with proxy headers enabled, and the pull request says which it assumed. They check that the test for a limited route does not leave the window full for the tests after it in the same module, and that coverage report in the session still clears the threshold the workflow enforces.

The session's transcript keeps the test run with the 429 visible, the coverage table, and the ruff, mypy and ty output, so a reviewer who wants to see the strict type check pass does not have to run it.

Run it on every new anonymous endpoint

Add an automation to the Project with a GitHub trigger on pull_request.opened and pull_request.synchronize, and a prompt that asks the agent to check whether the pull request adds a route without CurrentUser and, if so, to push the limiter dependency onto it with a test. Only work by an owner, member or collaborator of the repository fires the trigger; pull requests from forks never do.

For a one-off across many services, a script on your server sends one POST /o/{org}/sessions per repository with this prompt, and a webhook_url on the API key receives run.completed with the session_url of each pull request.

The use case this job follows, with its automation and trigger: Hand off from your terminal.

Why run it on Rush?

Rate limiting is the change everyone agrees on and nobody gets to, because the first hour goes to questions: which routes, which key, what about the proxy, what about workers. The agent answers them from the repository itself, writes the dependency and the tests in its own sandbox with Postgres running, and your laptop never runs the suite.

What comes back is a diff with a test that shows the 429, a coverage number that still clears CI, and the typing checks already green in the transcript. The caveat about workers is written down where the reviewer will read it instead of discovered in production.

Frequently asked questions

Why not slowapi or another library?

Either works, and if you name one in the prompt the agent uses it. Without Redis in the stack a library brings an in-memory store anyway, so a dependency of a few dozen lines that mypy --strict and ty can check cleanly keeps the surface small. The pull request says this, and switching the store later is a change inside app/core/rate_limit.py only.

The container runs four workers. Does the limit still hold?

Per worker, yes; across the container, a client can get up to four times the configured number before every worker's window is full. The agent reads --workers 4 in backend/Dockerfile and writes this into the pull request rather than pretending otherwise. For one shared counter, use Traefik's ratelimit middleware in compose.yml or add a Redis store, both of which a follow-up message can do.

What does a limited request get back?

A 429 with a JSON detail and a Retry-After header holding the seconds until the window has room. The test asserts both, so the frontend under frontend/ can show a wait message rather than a generic error.

Will the tests slow down or interfere with each other?

The limiter has a reset used by a fixture, so each test starts with an empty window, and the test that fills a window does it with a low limit set through the Settings override rather than hundreds of requests. The module-scoped client fixture from conftest.py is reused as the other route tests do.