Firestore does not bill for queries. It bills for documents returned, plus index entries in batches of a thousand, with a floor of one read per query even when the query matches nothing. The free tier is 50,000 document reads a day. Those four facts, from Firebase's own pricing page, explain almost every surprise invoice we are asked to look at.
The consequence founders find counter-intuitive: your bill tracks how your screens are written, not how many customers you have. A quiet week can cost more than a busy one did, because the cost was never really about visitors.
Find yours in ten minutes
Start with the queries that have no ceiling. This will over-report, because some collections are genuinely small, and that is fine: you are looking for the ones where the size grows with your success.
# Firestore reads with no limit anywhere on the line.
grep -rn --include=*.ts --include=*.tsx \
-E "getDocs\(|onSnapshot\(" src/ | grep -v "limit("For each hit, ask what that collection holds a year from now. Then open the Usage tab in the Firebase console and read reads per day against your actual traffic. If reads divided by daily active users is in the thousands, the number is being produced by how a screen is written rather than by demand.
The counting fix, which is most of the saving
Aggregation queries are billed on index entries rather than documents: roughly one document read per thousand entries scanned, so a count over 1,500 entries bills as two reads. The same answer, for a rounding error of the cost.
// N document reads, where N is the whole collection.
const snap = await getDocs(collection(db, "orders"));
const total = snap.size;
// About one read, whatever N grows to.
const snap = await getCountFromServer(collection(db, "orders"));
const total = snap.data().count;The rest of the list
- Close listeners when the screen closes. A subscription that outlives its component keeps charging for every change to the collection for as long as the tab is open.
- Paginate rather than filtering client-side. Pulling a collection into the browser to filter it in JavaScript bills for every document the filter throws away.
- Put an index behind every query you sort or filter on, so the query reads entries rather than scanning documents.
- Set a budget alert and a hard spend cap today, before any of the rest. It is five minutes and it converts the worst case from an unbounded invoice into a bounded outage.
Done together these usually take a week and typically cut reads by an order of magnitude, because the saving is concentrated: two or three screens are almost always responsible for most of the bill, and they are the screens somebody built early and nobody revisited.
When the answer is not optimisation
Sometimes the reads are not a bug. If your product is genuinely reporting, and the queries you need are joins and aggregates across collections, you are paying a document database to do a job it was not built for, and every workaround is another denormalised copy that can disagree with the original.
That is a real reason to move, and it is a different decision from a cost bug. We work through both the same way on a Firebase app going to production: fix what is cheap to fix first, then put real numbers against the move, so leaving stays a decision rather than a reaction to an invoice.
Written by Kato Chojiro
Takes AI-built MVPs to production: security, data, and the infrastructure underneath.
Connect on LinkedIn