Buckets OSS
Buckets answers one question: for every database read your app makes, where did it go? It attributes every read to the feature that served it, shows you the effect of a change before and after you ship it, and — connect Slack — pages you the moment a real read spike begins, before the invoice is in the post. The collector is open source; this page is how you read it inside Crossdeck and how the cost alerts work.
TL;DR
- Every read is caught and attributed. Buckets is the meter — it counts reads at the source, so its number is authoritative. Reads it can't attribute to a feature surface honestly as
unknown. - It's a before/after instrument, not wallpaper. Mark the change you shipped and watch each bucket's reads go from before to now. Raw operation counts only — verify the saving on your provider's bill (the source of truth for money).
- Name an
unknownbucket with one line:bucket("nightly-export", fn). Drill and tag until the source is named. - Get paged in Slack on a real spike — after a ~7-day baseline, only on a genuine deviation, once per episode, with a one-click "expected, quiet for 24h."
- It never costs you reads. Every part reads a small maintained summary — nothing scans.
- Read it back locally, no account needed. The collector writes a live readout to
.crossdeck/buckets.md— open it or ask your AI session to "read me my buckets." Sign up and the same numbers surface here, with the drill-down, before/after, and spike alerts. - Not just Firestore — MongoDB and Postgres too.
installMongoMeter(...)counts the documents your reads return;installPgMeter({ Client })counts the rows your queries return — one adapter covering Supabase, Neon, Vercel Postgres, RDS and plain Postgres. Both attribute to the samebucket()paths. Each database is measured in its own raw unit — Firestore in reads, MongoDB in docs read, Postgres in rows read — never a dollar bill; the dashboard renders whichever language your project speaks.
Where your reads go
Find Buckets under Errors → Buckets OSS. The main view ranks your buckets by reads, so the heaviest surface is the longest bar. A bucket is either tagged — attributed to a named feature you wrapped — or untagged, shown in amber, meaning Buckets caught the read and labelled it by collection (col:events) but no feature name has been set on that path yet. Untagged is never a blind spot; it's a to-do with a one-line fix (see Name a bucket).
Before and after a change
The point of Buckets isn't to admire a number — it's to answer "I made a change, did it work?" Set the change-date marker to the day you shipped, and every bucket shows its reads before the change (a faint track) and now (the solid fill). A surface you fixed visibly shrinks; one you didn't sits unchanged. The headline tells you the whole story at a glance:
// the headline after shipping a maintained-read fix
2,358,918 → 1,632,118 reads/day // −31% · −726,800 reads/day
Switch the trend to hourly to watch a fix land this hour — you don't have to wait for tomorrow's number, or spend another day's reads to find out it worked.
Name a bucket
This works exactly like custom events: auto-capture is free, and you name the things that matter. When a bucket shows as unknown, wrap that read path in bucket() and every read inside it is attributed to that name:
import { bucket } from "@cross-deck/buckets";
// every read inside here is attributed to "nightly-export"
await bucket("nightly-export", async () => {
const rows = await db.collection("events").where(/* … */).get();
});
Two grains: tag a whole surface — bucket("pulse-map", handler) — or tag a single query — bucket("owner-lookup", () => db.doc(id).get()). Drill in, ship, look again, and keep going until the read is named down to the line you care about.
Tagging applies going forward, not backward. A count is a fact at the moment it happens — Buckets never rewrites history. So after you ship a tag, the old unknown / col:* bucket won't rename itself; a new named bucket appears and grows as fresh reads land, while the unnamed one stops climbing. The next full day shows the path named from its first hour. Watch for the new name appearing — not the old bar changing colour.
Tagged it, but it's still showing untagged? You tagged the wrong path. The check after you ship is simple: did your new name start growing while the col:* bucket stopped climbing? If instead the untagged bucket keeps climbing and your name never grows, the reads aren't flowing through the code you wrapped. The collection tells you what is read, not where — and the same collection is usually read from several places. Re-evaluate which path actually issues the reads and tag that one. A tell: a smooth, constant rhythm — especially with nobody using the app — is a machine (a scheduled job or per-event processing), not a person; a spiky, daytime pattern is user-driven. Let the shape point you at the right path.
Slack alerts: paged before the bill
This is the part that turns Buckets from a dashboard you remember to open into a system that finds you. Connect Slack (Developers → Integrations → Slack) and Crossdeck watches your buckets and pings you the moment a real read spike starts:
🟡 Read spike detected — 512,000 reads in the last hour, about 10× your normal for this time of day (~50,000). Open Buckets to see which bucket moved.
An ongoing spike pings once, not every hour. Shipped a feature you know adds reads? One click — "Expected — quiet for 24h" — and it hushes while the baseline re-learns. Your knowledge of your own roadmap is the final authority; the system never pretends to know better.
No Slack connected? Alerts still appear in the Buckets dashboard's Read-spike alerts card — connecting Slack just adds the ping.
How the baseline learns you
An alert is only trustworthy if it never cries wolf, so the detector is deliberately conservative:
- It learns first. For ~7 days it builds a baseline and fires nothing. The dashboard shows the progress — "baseline forming · day 3 of 7" — then "baseline armed."
- It knows the time of day. A separate baseline per hour-of-day, so a busy 2pm is judged against other 2pms — never against 3am. This is what stops the evening from looking like an anomaly every night.
- It follows your fixes. The baseline is recency-weighted: arrive bleeding 2M reads, fix down to 50k, and it forgets the 2M and settles at your new normal — so a later 50k→500k spike is a real deviation, not lost under an old number.
- Two gates. A spike must be both a large statistical jump and a meaningful multiple of normal, so a tiny bucket jittering by a few reads can never page you.
It never costs you reads
The one rule that holds the whole system together: the thing that watches your read bill must never run one up. The meter counts in memory and writes a small summary about once a minute. The baseline and the alert detector read that maintained summary — never your data. Nothing in Buckets, anywhere, scans. A cost tool that costs is a contradiction; this one can't.
What the collector sees — and what Crossdeck adds
The collector is open source and yours to run free. It's honest about its own edges — and each one is exactly what Crossdeck is for:
- It sees the surface you install it on. On your server it catches server reads — often the minority; most apps also read straight from the browser (a separate install). Your bill is the sum of both. Crossdeck stitches server + browser + every surface into one number, so you're never reasoning from a slice.
- Reading the numbers back yourself costs a few reads — querying your own stored rollups is still a read. Inside Crossdeck you read a small maintained summary, so looking is free and live — the cost tool never costs you to check.
- The collector is the raw meter; Crossdeck is where it becomes action — the drill-down (tag → next-biggest → tag again), the before/after verdict, the 7-day baseline, and the Slack alerts above.
The numbers are identical on both sides — same counts, same source. Crossdeck just makes them whole, free to read, and impossible to miss. Onboard once, install the SDK, and read-cost comes with it — no second setup.
Related
- Track custom events — the same name-what-matters pattern, for analytics.
- Capture an error — the other half of "page me before it's a problem."
- Limits and quotas — the platform limits Buckets helps you stay inside.