turso usage analytics — reading the read budget #
How to pull usage/cost analytics straight from the turso CLI, what each
command reveals, and — most importantly — how to use --queries
attribution as a lagging signal on whether our architecture is
drifting toward corpus-proportional work.
The dashboard graphs (Rows Read / Rows Written / Syncs / Storage, last 30 days) are pretty but they only tell you that a number moved. The CLI tells you which query moved it. That's the difference between "reads spiked on May 16" and "the stats aggregate is a full table scan and runs hourly."
the three commands #
All read-only, all hit the control plane (api.turso.tech), all use the
CLI's browser-auth session — no tokens to plumb. Numbers are
period-to-date for the current billing window (resets monthly).
1. turso plan show — org-wide quota dashboard #
The closest CLI equivalent to the dashboard's headline tiles.
$ turso plan show
Organization: personal
Plan: scaler
Overages enabled
RESOURCE USED LIMIT LIMIT % OVERAGE
storage 3.6 GB 24 GB 15%
rows read 4996.6M 100000M 5%
rows written 45.2M 100M 45%
embedded syncs 6.3 GB 24 GB 26%
databases 4 1000000000 0%
locations 1 6 17%
groups 0 6 0%
Quota will reset on Sun, 31 May 2026 19:00:00 CDT
Use it for: "are we anywhere near a quota wall, and when does the meter reset." On Scaler the answer is almost always "no" — at this snapshot we're at 5% of reads and 45% of writes. Writes are the tighter budget.
2. turso db inspect <db> [--verbose] — per-database totals #
Same metrics scoped to one database; --verbose breaks out by location.
$ turso db inspect typeahead --verbose
Total space used: 3.2 GB
Number of rows read: 3293904957
Number of rows written: 45005088
Embedded syncs: 6.3 GB
LOCATION TYPE ROWS READ ROWS WRITTEN TOTAL STORAGE BYTES SYNCED
aws-us-east-1 primary 3293904957 45005088 3.2 GB 6.3 GB
Use it for: attributing the org total to a specific database (we have 4).
typeahead alone is 3.29B of the 5.0B org reads.
3. turso db inspect <db> --queries — per-query attribution ⭐ #
The one the dashboard can't do. Lists each distinct query (parameterized, so all bind-value variants collapse to one row) with the rows it read and wrote over the period. This is where you find the expensive patterns.
$ turso db inspect typeahead --queries
Snapshot 2026-05-24, top read consumers:
| rows read | query | what it is |
|---|---|---|
| 102.0M | SELECT … FROM actors WHERE handle != '' AND rowid > ? ORDER BY rowid LIMIT 2000 |
local-replica sync pagination walking the whole table |
| 74.6M | SELECT COUNT(*), SUM(CASE…) FROM actors |
stats aggregate, full scan every run |
| 48.2M | (10-column variant of the sync walk) | older/parallel sync path |
| 18.6M | … WHERE handle IN ('bad-example.com','aoc.bsky.social','zzstoatzz.io') |
ad-hoc freshness probes doing full scans |
Writes attribute cleanly too: the last_activity_at batch UPDATEs and the
actors upsert dominate, which is expected for an ingester.
the gap: no daily time series #
The CLI gives point-in-time / period-to-date totals. It does not
expose the day-by-day series behind the dashboard graphs — there's no
--daily / --since flag. If we ever need the time series
programmatically (e.g. to alert on a read-rate inflection), that lives in
the platform usage API the dashboard consumes, not the CLI. For "where is
the budget going right now," --queries is strictly more useful than the
graphs.
why this is a lagging signal on our design #
Our redesign principle is "no request path may do work proportional to
corpus size." --queries is the audit that tells us whether we're
actually holding that line, because a corpus-proportional pattern shows up
as a query whose rows read grows roughly linearly with the actor count
(~5.9M today) every time it runs.
Read the table above through that lens:
- The 102M + 48M sync walks are corpus-proportional by design. Each full sync reads all ~5.9M rows; do it ~25× over a month and you get ~150M. This is the App-B local-replica bootstrap / incremental resync — not a request path, and not something the prefix-index work touches. It's acceptable as a batch cost, but it's the reason "syncs" and "rows read" both spike together. If it ever creeps into something triggered per-request, this is where we'd catch it.
- The 74.6M stats aggregate is a full scan on a hot path. A
COUNT(*) + SUM(CASE…)overactorsreads the whole table every timematerializeStatsruns. That's textbook corpus-proportional work on a cron path. The fix is theactor_deltasrollup table (already in the schema) — increment counters on write, never scan. Worth confirming the full-scan version is actually retired; 74.6M says it isn't, or wasn't recently. - The 18.6M freshness probes are index-bypass waste. Three handles
should be three index seeks (~3 rows read), not 18.6M. The cause is the
actors.handleindex beingCOLLATE NOCASE; an ad-hocWHERE handle = '…'/handle IN (…)without a matchingCOLLATE NOCASEcan't use it and falls to a full scan. These are our own manualturso db shellchecks. Cheap to fix: appendCOLLATE NOCASEto interactive handle lookups. (See project memory: actors.handle index is COLLATE NOCASE.)
how to use it going forward #
Run turso db inspect typeahead --queries periodically (start/end of a
billing window, or after shipping a path that touches actors) and ask:
- Any new query with rows-read in the tens-of-millions? That's a corpus-scan that slipped in. Find it, decide if it belongs on a batch path or needs an index / precomputed structure.
- Did a known full-scan's number drop after we shipped a fix? This is
how we verify the stats-rollup migration actually killed the
COUNT(*)scan — the number should fall toward zero next period. - Is
rows writtenclimbing faster than ingest volume? Points at a write-amplifying upsert or an over-eagerlast_activity_atchurn.
It's a lagging signal — it tells you what already happened, after the
meter ran. But it's the cheapest possible audit of "is the system still
shaped the way we think it is," and it needs zero instrumentation in our
code. Pair it with the prefix-index work: once serving is bounded, the
only corpus-proportional reads left in this table should be batch jobs
(sync, snapshots), and --queries is how we'll prove that.
someday: a skill #
This is a good candidate for a /turso-audit skill — run the three
commands, diff --queries against the last snapshot, and flag any query
whose rows-read crossed a threshold or grew vs. the prior period. Not yet;
documenting the manual procedure first.
reference #
turso db inspect --help(aliases:usage); flags--verbose,--queriesturso plan show- numbers are period-to-date, reset monthly (see
plan showfooter) - related:
docs/turso-research.md(capabilities / export wire format), project memory actors.handle index is COLLATE NOCASE