# Investigate Slow API Routes in a Next.js App

Yes. An agent reads the route handler, its queries, the schema and recent commits of your Next.js app and writes incident.md with ranked suspects and checks.

[Run this on Rush](https://cloud.getrush.ai/run/investigate-slow-api-routes/nextjs/), in a Project that has your repository.

## What the agent does

The agent starts at the route the monitor names. In a Next.js app like umami-software/umami that is src/app/api/websites/[websiteId]/stats/route.ts, a GET handler that calls parseRequest with a Zod schema built from filterParams and withDateRange, checks canViewWebsiteSection for the overview and compare sections, builds filters with getQueryFilters, and then calls getWebsiteStats twice: once for the requested range and once for the comparison range from getCompareDate, with compare defaulting to prev. Two aggregate queries per request is the first thing the report records, because any slowdown in the query shows up doubled.

Then it follows each call into the libraries. parseRequest in src/lib/request.ts runs checkAuth, which in src/lib/auth.ts parses the bearer token and calls getUser with includePassword on every request to compare a password fingerprint. getQueryFilters calls setWebsiteDate, which fetches the website through src/lib/load.ts: cached in Redis for 86400 seconds when REDIS_URL is set, otherwise a Prisma query each time. A segment or cohort parameter adds a getWebsiteSegment lookup. getWebsiteStats in src/queries/sql/getWebsiteStats.ts goes through runQuery in src/lib/db.ts, which picks the ClickHouse query when CLICKHOUSE_URL is set and the Prisma raw query against Postgres otherwise. The Postgres branch groups website_event by session_id and visit_id over the date range; when any event filter is present it switches to a second shape with count(*) and a left join on custom events, and parseFilters in src/lib/prisma.ts adds an inner join on session as soon as a filter names a session column or referrer. The ClickHouse branch reads the website_event_stats_hourly materialized view when there are no event filters and the raw website_event table when there are.

The third pass is schema and configuration. prisma/schema.prisma indexes website_event on websiteId with createdAt, and on websiteId, createdAt and each of urlPath, urlQuery, referrerDomain, pageTitle and eventName, plus websiteId, sessionId and createdAt, so the report can say which predicates in the generated SQL fall outside an index. prisma/migrations ends at 26_add_api_key and db/clickhouse/migrations at 14_add_performance_projections, which tells the agent what the last deploy may have changed on each database. package.json pins next 16.3.8, @prisma/client 7.10 with @prisma/adapter-pg, @clickhouse/client, kafkajs and pnpm 12.3.4, and next.config.ts, the Dockerfile and docker-compose.yml show which of these a deployment actually turns on.

Finally it reads git log for the last 30 commits with their diffs, looking for changes under src/queries/sql, src/lib/prisma.ts, src/lib/clickhouse.ts, prisma/migrations and db/clickhouse, and lines them up against the deploy the monitor points at. Where a check can run in the sandbox it runs: pnpm install, pnpm generate:db:client, and the existing src/queries/sql/getWebsiteStats.test.ts under vitest to print the exact SQL the filters produce. It changes no code. incident.md carries a timeline, then each suspect with the file and line it rests on and the EXPLAIN, log query or environment check that would confirm or clear it.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Incident: a Datadog monitor shows p95 latency of the website stats API route rising from 300 ms to 2.4 s after the last deploy, with no traffic change. Read the code, the configuration and the last 30 commits, find the most likely causes, and write incident.md with a timeline, each suspect with file and line evidence, and the check that would confirm or clear it. Change no code.",
  "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 incident.md as an artifact on the session, with a download link the API returns for an hour at a time. For a codebase shaped like umami it opens with a timeline built from the commit dates and the deploy, then a ranked list of suspects. A typical entry names the doubled getWebsiteStats call in the stats route, the inner join on session that parseFilters adds for a referrer or session-column filter, the switch from website_event_stats_hourly to the raw table when an event filter is present, or a website fetch that stopped hitting Redis because REDIS_URL was dropped from the deployment. Each entry quotes the file and line and ends with its check: an EXPLAIN ANALYZE of the generated query with the production filters, a count of requests carrying a segment parameter, or a look at the environment of the deployed container.

The session record holds every file the agent opened and every command it ran, so a reviewer can see that a claim about the query shape came from reading getWebsiteStats.ts rather than from memory. The final message is the summary; the artifact is the report; the transcript is the evidence.

Before acting on it, the on-call engineer runs the top suspect's check against the real database or Datadog, because the sandbox has the code and schema but not production data. A suspect the check clears goes back to the same session as a follow-up message, and the agent revises incident.md with the next candidate.

## Run it on every latency monitor alert

Datadog cannot call a Rush trigger directly, so a small handler on your server receives the monitor's webhook, puts the monitor name, the route and the p95 values into this prompt, and sends POST /o/{org}/sessions with your repository in resources. Use the monitor's event id as the Idempotency-Key so a monitor that re-notifies does not start a second investigation.

Give the API key a webhook URL and the run.completed event arrives with session_url when the report is ready; the handler reads the session, takes the artifact link for incident.md and posts it to the incident channel, so the first pass is waiting before anyone opens a laptop.

## Why run it on Rush?

A latency investigation on a Next.js app with two query backends means reading the Prisma branch, the ClickHouse branch, the filter parser and the schema side by side, then checking what the last deploy touched. On a laptop that competes with the incident itself. In a Rush sandbox the agent clones the repository, installs it, generates the Prisma client and runs the existing query tests to print the real SQL, with none of that touching the machine of the person on call.

Every command and its output stays in the session, so the report is checkable rather than a guess, and several routes or several services can be investigated at once, each in its own sandbox, up to the key's concurrency limit. The engineer gets a ranked list with checks instead of a blank terminal.

## Frequently asked questions

### Does the agent connect to my production database or Datadog?

Not in this request. It works from the repository, the schema, the configuration files and the commit history, and writes the check that would confirm each suspect for you to run against production. Add a Datadog MCP server in connections if you want it to pull the monitor's timeseries and traces itself.

### Which database path does it analyse, Postgres or ClickHouse?

Both, because runQuery in src/lib/db.ts chooses at runtime from CLICKHOUSE_URL and DATABASE_URL. The report says which branch your deployment runs, from the Dockerfile, docker-compose.yml and environment files it can read, and ranks the suspects for that branch first.

### Will it change code or open a pull request?

No. The prompt ends with Change no code, and the result is incident.md. Once the report names the cause, a follow-up message to the same session can ask for the fix, and that turn opens the pull request with the test that covers it.

### How does it know what the last deploy changed?

From git log over the last 30 commits with their diffs, filtered to the query, library, schema and migration paths, and from the last entries in prisma/migrations and db/clickhouse/migrations. If your deploys are tagged, say so in the prompt and it reads the range between the two tags instead.
