- Rows read by feature ranks your heaviest queries without an EXPLAIN sweep.
- One pg adapter covers Neon, RDS, Supabase, and Vercel Postgres.
- Observe the rows already returned — never add a profiling query to production.
Definitions used in this guide
A single document, row, or query result your database counts toward usage and bills you for.
Connecting each read back to the feature or code path that caused it, instead of seeing one undivided total.
Where a read ran — your server, your web app, your dashboard, or a mobile build. The same query can fire from several, and the bill hides which.
What should be true before you start?
There are two ways to find heavy queries: ask the database to profile itself, or observe the rows your queries already return. The first adds load to production and needs access you may not want to grant. The second is free — the row count is already in every result — and it is enough to rank your queries by the work they do.
Read cost is a measurement problem before it is an optimization problem. You cannot cut what you cannot attribute, and a database bill is a single total with no memory of which feature, screen, or background job spent it. The first job is to make the reads legible — grouped by the part of the product that caused them — so the expensive path is obvious instead of theoretical.
- Decide you want ranking by load, not a one-off EXPLAIN on a single statement.
- List the endpoints and jobs you suspect of reading too much.
- Name the buckets you want rows-read grouped under.
How do you find where the reads go?
The Postgres meter patches the pg client's query path once and reads the row count every result already carries. It covers Neon, RDS, Supabase, and Vercel Postgres through the same driver, attributes each SELECT's rows to the active bucket, and never issues a query of its own.
The honest version of this measures the work already in hand. A good read meter counts the documents or rows a query already returned — it never runs an extra query, an EXPLAIN, or a profiler scan to measure, because a cost tool that itself costs reads is worse than none. Counting stays in memory, and attribution rides on the name you gave the path.
- Install the meter with your pg
Client; it covers Neon, RDS, Supabase, and Vercel Postgres. - Wrap each suspect endpoint in a named bucket.
- Let every
SELECTreport the rows it returned, grouped by bucket. - Rank buckets by rows read — your heaviest queries sort to the top.
- Add an index, filter, or pagination to the top one and confirm it drops.
| Approach | Cost to production | What you get |
|---|---|---|
| EXPLAIN / profiler scan | Adds load; needs DB access | Deep plan for one statement |
| Rows-read attribution | None — reads the result in hand | Every query ranked by load |
| The bill alone | None | A total with no query breakdown |
import { installPgMeter, bucket } from "@cross-deck/buckets"
import { Client } from "pg"
installPgMeter({ Client })
await bucket("search>results", () =>
pool.query("SELECT * FROM products WHERE name ILIKE $1", [term]))
Where do teams get this wrong?
The mistake is reaching for a profiler when a row count would already tell you which query to look at.
Most read-cost surprises are not one greedy query; they are an unattributed one. A scheduled job that re-reads a collection every few minutes, a dashboard that re-scans on every refresh, or a listener that fans out on each change can outspend every user-facing feature combined — and none of it shows up until you group the reads by cause and one bar dwarfs the rest.
- Running EXPLAIN ANALYZE on production to hunt a query you have not located yet.
- Optimizing a query that is slow but rarely run, while a frequent broad SELECT does the real damage.
- Assuming the slowest query and the heaviest query are the same — they often are not.
How does Crossdeck Buckets surface this?
Crossdeck Buckets ranks Postgres queries by rows read across Neon, RDS, Supabase, and Vercel Postgres, so the heavy ones surface themselves. Because it observes the rows already returned, it never adds a profiling query to the database you are trying to relieve.
You go from “something is heavy” to “this search query returns the most rows, on the server” — a specific, fixable target instead of a profiler safari.
This is also the upgrade path, and it stays free across the step. The open-source collector shows the reads on one surface, grouped by the buckets you named — no account needed. Sign up to Crossdeck and a single SDK install adds the dimension the collector alone cannot see: which environment each read ran in — your server, your web app, your dashboard, or a mobile build — folded into the same buckets, still free. A spike stops being a guess between “is it the backend or the client?” and becomes a labelled segment you can read at a glance.
What should a healthy setup let you do?
After instrumenting, you should be able to open one view and name the top three features by read load, point to the single path driving the biggest bar, and say which environment it ran in. If that still takes a spreadsheet and a guess, the setup is not finished.
A healthy setup also makes the next change cheap to verify. Shipping an index, a cache, or a narrower query should move a specific bucket down — and you should be able to see that it did, not infer it from next month’s invoice.
- Rank features by read load and find the biggest single path.
- See which environment a read ran in — server, web, dashboard, or mobile.
- Confirm a fix moved the right bucket down, not just the bill as a whole.
What should you review after it is running?
Review the biggest bucket first — the single largest source of reads is almost always where the cheapest win lives. Then look for the rhythm that does not match your users: a steady overnight wave with nobody on the app is a machine, not a customer, and machines are the easiest reads to cut.
Treat the read meter as an operating surface, not a one-time audit. Each spike, each new feature, and each background job is a chance to confirm the cost is attributed before it compounds.
- Start at the largest bucket; that is where the cheapest win usually is.
- Watch for read patterns with no matching user activity.
- Re-check attribution whenever you add a feature or a scheduled job.
How should the whole team use it?
Read cost is not only an engineering concern. A founder watching runway wants the trend and the biggest line item. An engineer wants the exact path and environment to fix. Both are reading the same buckets, just at different depths.
When the cost is attributed by feature and labelled by environment, the conversation changes from “the database bill went up” to “this job, on the server, doubled — here is the fix.” That is the difference between a vague worry and a one-line task.
- Founder: watch the trend and the largest cost driver.
- Engineering: jump to the exact path and environment to fix.
- Everyone: reason from one attributed view instead of a single total.
Frequently asked questions
Do I need EXPLAIN to find expensive Postgres queries?
Not to find them. Rows read ranks your queries by the work they do without touching production. Use EXPLAIN afterwards on the one query you have already identified, if you want the plan detail.
Does one adapter really cover Neon, RDS, and Supabase?
Yes. They all use the pg driver, so a single Postgres adapter attributes rows read across Neon, RDS, Supabase, Vercel Postgres, and plain Postgres.
Is the slowest query also the most expensive?
Not always. A slow query that runs rarely can cost less than a fast query that runs constantly and returns many rows. Ranking by total rows read captures the load that actually adds up.
Does Crossdeck work across iOS, Android, and web?
Yes. Crossdeck is designed around one customer timeline across Apple, Google Play, Stripe, and web or mobile product events, so the same entitlement and revenue model can travel across surfaces.
What should I do after reading this guide?
Use the CTA in this article to start free or go straight into read the buckets docs so you can turn the concept into a verified implementation.
Take this into the product
Rank your heaviest Postgres queries by rows read, free, and see which environment each one runs in.