What the agent does
The agent starts by getting the site running with real data, because an N+1 only shows when a page lists more than one object. In a Django project like django/djangoproject.com it reads requirements/prod.txt (django==6.1.1), pyproject.toml (Python 3.12), the Makefile, whose APP_LIST names the apps (accounts, aggregator, blog, dashboard, docs, foundation, fundraising, members, releases and others), and docker-compose.yml, which brings up Postgres 17. django-debug-toolbar 7.0.0 is already a dev dependency, and djangoproject/settings/dev.py adds it to INSTALLED_APPS and the middleware when it can be imported, so the toolbar's SQL panel is ready in the sandbox's browser.
It does not take the code's word for which pages are fine. The project already eager-loads in places, for example members/views.py with prefetch_related("members") and the dashboard's select_related("category"), and has six assertNumQueries calls. So it measures: it creates enough rows per model (feed items across several feeds, members with invoices, meetings with business items), opens each main list and detail page, and records the query count from the toolbar and from django.test.utils.CaptureQueriesContext. A count that grows with the number of rows is an N+1; a fixed count is not.
Then it fixes the ones it found at their source. A typical case in this codebase: the foundation MeetingDetail view prefetches business, but get_context_data then calls meeting.business.filter(business_type=…) twice, and .filter() on a prefetched relation ignores the prefetch and queries again. The fix is a Prefetch with a filtered queryset and to_attr, or filtering the prefetched list in Python. Members' is_invoiced, is_paid and get_expiry_date each call self.invoice_set.all(), which costs one query per member wherever a template calls them in a loop; the admin already covers this with prefetch_related("invoice_set") and the agent checks whether any public view does too.
For every page it measured, fixed or already correct, it adds a test with assertNumQueries(n) around a request to that page, with enough rows that an N+1 would change n. It runs the suite with make test against Postgres to confirm nothing else changed, and records each page's count before and after.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Find the N+1 queries on this Django app's main pages. Start the app against Postgres with enough data that each list shows several objects, then measure each main list and detail page's query count with django-debug-toolbar and CaptureQueriesContext. A count that grows with the number of rows is an N+1. Fix each at the queryset with select_related, prefetch_related or Prefetch objects, including cases where a template or method calls .filter() on a prefetched relation. Add one test per page that uses assertNumQueries with several rows, so the test fails if the count grows. Run the full test suite. Open a pull request with a table of each page's query count before and after.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- 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.
- 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.
- 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 with three kinds of change: querysets in views and managers that now select_related or prefetch_related what their templates read, methods or templates changed to use the prefetched data instead of re-querying it, and new tests in each app's tests.py that request a page with several rows inside assertNumQueries. The description has a table of pages with their query counts before and after, measured on the same data, and lists pages that were already correct and only gained a test.
The pull request's own checks run the suite. The trajectory keeps the commands that seeded data and started the site, each page's captured queries, and the test run, so a reviewer can see the repeated query that justified each change.
Before merging, a reviewer checks that each prefetch loads only what the page shows (prefetching a large relation on a list page can cost more than the N+1 it removed), that assertNumQueries counts are not so tight that an unrelated session or cache query breaks them, and that a changed method still behaves the same when called outside the list view.
Run it on every slow-page alert
When your APM or error tracker flags a view for too many queries or a slow transaction, send this request with the view's URL and the alert's numbers in input and the repository in resources. The agent starts from that page instead of surveying all of them, and the test it adds keeps the regression from coming back.
Give the API key a webhook URL and Rush POSTs run.completed or run.failed when the turn ends, with the session URL to read the pull request link from. Use the alert's id as the Idempotency-Key header so the same alert firing twice starts one run.
The use case this job follows, with its automation and trigger: Fix a bug from an alert.
Why run it on Rush?
N+1 queries hide in development because a fresh database has two rows per table and every page feels fast. Finding them takes realistic data, a running site and a query count per page, which is tedious on a laptop and easy to skip. On Rush the agent sets up Postgres and the data in its own sandbox, opens each page in a browser with the toolbar, and keeps the captured queries in the trajectory as evidence for every change.
The tests are the lasting result. Each page gets an assertNumQueries guard with several rows, so the next template change that reads a relation in a loop fails in CI rather than in production. And because each run is independent, several apps or repositories can be audited at once, each in its own pull request.
Frequently asked questions
Will it add django-debug-toolbar to production?
No. It uses the toolbar in development settings only, as djangoproject.com already does, and the tests use Django's own assertNumQueries. If your project has no toolbar, the agent measures with CaptureQueriesContext and leaves your dependencies alone unless you ask for the toolbar.
Why does a prefetch sometimes not help?
Because .filter(), .order_by() or .exclude() on a prefetched relation creates a new queryset and goes back to the database. The fix is a Prefetch object with the filtered queryset and to_attr, or filtering the already loaded list in Python. The agent checks for this pattern explicitly, since it looks eager-loaded and is not.
Won't assertNumQueries tests break whenever we add a query?
They break when a page's query count changes, which is the point; the failure message lists the queries, so an intended change is a one-number update. The agent writes them with several rows so the count is fixed regardless of data size, which is what separates them from brittle tests.