Request Timing
The Overview of a PHP site carries a Request timing section that reads the always-on nginx access feed to show how the site is responding as you work, no debug bridge needed. A range picker (15 minutes up to 7 days) drives the whole view: headline figures for the typical and p95 response times, the request count and error rate, a response-time distribution, a throughput chart, the slowest routes, and a table of every route with its p50 and p95. A Recent requests tab lists the latest calls with their time, method, path, status, and duration.
Routes are grouped after collapsing id-like path segments, so /users/123 and /users/456 aggregate as one GET /users/:id entry, and query strings are dropped before anything is recorded. Requests nginx serves without the app are left out: static assets by file extension, anything with a zero request time (a static file nginx answers directly, like manifest.json or robots.txt), and upgraded connections such as WebSockets, so a page's dozens of asset requests don't drown out its app routes. Anything a dev server answered is left out on the same grounds: it serves under its own prefix on the site's domain, so without this its modules arrive looking like routes the app served, and the ones it rebuilds on every save would read as the busiest routes on the site. An upgrade is logged once, when the socket closes, carrying the whole lifetime of the connection as its request time, so a long-lived Reverb or Vite HMR socket would otherwise read as one route taking thousands of seconds. The first request after a site has sat idle past the idle-suspend timeout is treated as a cold start: its wake cost is kept out of every timing figure (the site and per-route percentiles and the distribution) while still counting toward the request total, and it's marked in the Recent list, so a wake never makes a route look slow. The last-seen time is seeded from the durable store on startup, so a wake right after a daemon restart is still recognised as cold rather than counted as warm. Requests are written to a small SQLite store in the data directory, so the history survives a restart and any range up to the seven-day retention window can be read back; the watcher also keeps an in-memory window, which is what the doctor and slow-route notifications read. The same table is available to AI assistants over MCP as diag route_timing, and diag optimize_route pairs each slow route with the N+1 and slow queries captured against it.
The same flagged routes also surface as a Response Time warning in the site doctor (lerd site:doctor, the dashboard doctor card, and the MCP diag site_doctor action), so the nudge reaches you even when you're on another tab. The doctor reads the watcher's snapshot rather than re-measuring, so it stays quiet on a healthy or idle site. If you've enabled notifications, a route crossing the threshold also fires a slow_route push. It's edge-triggered: one push when the route goes slow, then it rearms once the route drops back within the typical band, so you're told again if it regresses later (see Notifications).
This is a local, single-developer signal meant to catch a route that is dragging, not a production analytics system. Each flagged route carries a Profile button that does the whole handoff in one click: it arms the SPX profiler, waits for it to actually be armed, then opens the route in a new tab so that request is captured and switches you to the Profiler where the fresh capture lands on top. Profiling is global and stays off until you ask for it, so the button turns it on for every request until you turn it back off. A non-navigable route (a POST, say) can't be opened for you, so there the button just arms profiling and opens the Profiler for you to reproduce it (see Profiler).
A git worktree is timed as its own thing. Requests to feature-x.myapp.test are recorded against that branch, not against the main checkout, so switching the worktree picker re-scopes the whole panel to the branch you're on and its routes open and profile on the worktree's own subdomain. The worktree's traffic still counts toward the parent when the sites list is ordered by use, since the project is the same project. The doctor's Response Time check and the slow_route push follow the same rule when they run against a worktree.
When debug capture is on, each route also gains an Inspect queries button that jumps to the Debug tab's Queries lens filtered to that route, the one place that renders captured queries. The Debug lenses share a single search within a site's Debug view, so the filter carries over as you switch between Queries, Dumps, and the kind lenses, and the search matches the request path as well as the SQL and file. Captured queries only exist for requests hit while capture was on, so a route you haven't exercised with the debugger shows nothing until you reload it. See Queries for the capture itself.