Slow queries
Every read and write your application sends over HTTP is timed at the edge. When one takes longer than the slow threshold, or is refused, it is recorded — and the console's Slow queries page groups those records by query shape, worst case first.
A fast call records nothing. An empty log is the healthy one.
What a row means
Rows are grouped by **shape**, not by call. Before anything is stored, every literal in the query is replaced with ?, so these two record as one row:
await db.exec(q.task.where.ownerId('alice').orderBy('updatedAt', 'desc').limit(50));
await db.exec(q.task.where.ownerId('bob').orderBy('updatedAt', 'desc').limit(50));
That is the point of the grouping. A thousand calls that differ only in their arguments are one problem, and they appear as the one row that names it — with the median, the 95th percentile and the worst case across all of them.
The same redaction is also why the log is safe to keep: your values never reach it. A write records its verb, its table and its **column names** — never a cell.
The refusal that almost always means a missing index
On this storage, a filter or an ordering that no declared index covers does not fail. It scans,
one fetch at a time.
That is the one rule about r2db that differs from a database you run yourself, and it is invisible in a result — a query with no covering index returns the right rows, and keeps doing so until the table is large enough for the scan to run out of its fetch budget. Then it is refused.
So **Fetch ceiling** in the log is the signal worth acting on first. It means the call spent its whole R2 fetch allowance and was refused rather than left to burn the database's one thread. Run r2db explain on the query and declare the index it suggests:
r2db explain --database DATABASE_ID --query ./query.ts
The other refusals the log names:
- **Time ceiling** — it spent its millisecond allowance. Usually the quieter version of the same
problem: it is reading far more rows than it returns.
- **Page faults** — it gave up with pages still wanted, so it is reaching across a lot of storage.
Reduce what it touches, or split it.
- **Version conflict** — a write claimed a store version that had moved. Re-read and retry;
readModifyWrite does this for you.
A row with no refusal label was slow but within every ceiling. It is doing real work rather than being refused, and r2db explain is still the right next step: a scan that fits inside the budget is slow for the same reason one that does not is refused.
What is not recorded
- **Calls faster than the threshold.** The default is 250 ms. Your operator sets it per deployment.
- **Live queries.** A subscription is one admitted request and then a delivery every time its
result moves, so a single duration does not describe it. Reads and writes over HTTP are what this log covers.
- **Application facet requests.** Code you deploy runs behind your own routes, and its URLs are
yours rather than query shapes.
- **Anything older than seven days**, and anything beyond the most recent 500 samples per database.
- **Your own browsing in the console.** The data browser runs real, billed
reads against the same route your application uses, with the same ceilings — but it does not write to this log. The distinction is structural rather than a setting: your application reaches the query route with an API token, and a console session reaches it with a browser cookie it cannot mint. So this page stays an answer to "what is slow about my app", and your clicking never shows up in it.
What the timing includes
The clock runs at the edge, around the one call into your database: it covers the wait for any deployment in progress, admission into the database, the query itself, and every storage fetch it took. That is what your exec() waited for, less your own network leg to the edge.
It is measured there rather than inside the database on purpose. A database with nothing to do hibernates in about ten seconds and stops costing you duration; a dashboard that asked each one what it had been doing would hold them all awake and bill you for it. So the log rides the requests you are already making, and this page reads the control plane — never your databases. Open it and refresh it as often as you like.