Mistake One: Listening When I Should Have Been Querying
I'd used onSnapshot (real-time listener) for data that changed maybe once a day. Every open browser tab maintained a persistent connection to Firestore and downloaded every change to the document, even when the user wasn't looking at the page. 200 users with an average of 1.5 open tabs each meant 300 persistent Firestore connections.
The fix: use getDocs (one-time query) for data that doesn't need to update in real time. Only use onSnapshot when the UI genuinely needs to react to changes as they happen. I replaced about eight listeners with queries and the read count dropped by 90%.
Mistakes Two and Three
Mistake two: no query limits. I was fetching entire collections in some places — not large collections, but unlimited queries. When the data grew, so did the reads. Add .limit(50) to every collection query unless you have a specific reason not to.
Mistake three: no budget alerts. Firebase lets you set billing alerts at specific thresholds. I hadn't configured them. Ten minutes of setup would have caught the anomaly hours earlier. Now this is the first thing I do in every Firebase project.
Key takeaways
- Use getDocs for one-time reads and onSnapshot only for data that genuinely needs to update in real time — persistent listeners multiply your read costs by the number of open connections
- Add .limit() to every collection query as a default — unbounded queries are both expensive and a performance risk as collections grow
- Set Firebase budget alerts immediately on project creation — five minutes of configuration catches unexpected cost spikes before they become a problem
Conclusion
Firebase's free tier is generous but it has edges. Understanding those edges — and designing around them — is the difference between Firebase being cheap and Firebase being expensive. The architectural decisions that manage cost also improve performance.
Enjoyed this article?

Vivek Kumar Singh
Technical Expert · Full Stack Cloud Engineer · Tokyo, Japan