The product worked every morning and died every afternoon.
Not slowly. Not with degraded performance. Every read returned an error, the pages went blank, and then at some point overnight it started working again as if nothing had happened. It took me longer than I want to admit to see the shape of it.
The shape
Firestore's free tier allows 50,000 document reads per day. Not per hour, not burst-limited — a hard daily ceiling, reset at midnight Pacific time, which is 07:00 or 08:00 UTC depending on the season.
Once it is gone, every read fails with RESOURCE_EXHAUSTED. There is no
degradation, no throttling, no partial service. The database simply stops
answering until the counter resets.
For a product where guests open a page and watch a live view of something, reads are not a background cost. They are the product.
The arithmetic I should have done on day one
The page polls for state. Each poll is one read per viewer. That is fine, until you multiply it by the way the product is actually used.
| Poll interval | Viewers | Duration | Reads |
|---|---|---|---|
| 2s | 1 | 10 min | 300 |
| 2s | 20 | 10 min | 6,000 |
| 2s | 20 | 30 min | 18,000 |
| 2s | 60 | 30 min | 54,000 |
The last row is the whole daily quota, from a single busy event. And because the product is used for gatherings, viewers do not arrive one at a time — they arrive in a group, all pointed at the same page, all polling.
The unit that matters is not requests per second. It is reads per viewer-minute multiplied by peak concurrency, and I had never written that number down.
Three things that hid it
The root cause is embarrassing and took an hour to fix. The two days were spent getting to it, and that was down to three specific things.
The failure looked like a code bug. Blank pages and a client-side error is what a bad deploy looks like. I spent the first several hours reading a diff that had nothing to do with it, because "it worked this morning and I shipped something at lunch" is an extremely convincing story.
The recovery was automatic. By the time I sat down properly in the evening it had often started working again. A fault that heals itself overnight is almost impossible to reason about from logs alone, and it actively teaches you the wrong lesson — that whatever you last touched fixed it.
Nothing was watching the quota. I had error alerting. I did not have consumption alerting. By the time errors fire, you are already down; the useful signal was the read counter crossing 60% of the daily allowance four hours earlier, and nobody was looking at it.
What actually changed
The fix was not "upgrade the plan", although that happened too. Paying removes the ceiling but keeps the cost curve, and a cost curve that scales with viewers times poll frequency is a bad curve to own whether or not there is a hard limit on it.
Polling interval became a function of concurrency. A page with three viewers can afford to poll often. A page with sixty cannot, and does not need to — the state it is watching changes at human speed. Backing the interval off as viewership grows removes the quadratic term entirely.
The state that changes constantly moved off the document database. A counter that ticks is not a document workload. Reads of hot, ephemeral state belong somewhere priced for that pattern.
The alert moved from errors to consumption. Quota used against quota available, checked hourly, alerting at 60%. That alert has since fired twice on ordinary busy days, both times hours before anything would have broken.
One number went into the runbook: reads per viewer-minute. Any change that moves it is a change to the cost model, and gets thought about as one.
The general lesson
I do not think the lesson is "do not use free tiers". A free tier was correct for that stage, and the same failure at 10× the scale on a paid plan would have been a bill instead of an outage — arguably worse, and definitely quieter.
The lesson is that a managed service's pricing unit is a design constraint, and you should be able to state it as a formula before you launch. Firestore charges per document read. That single fact should have produced the table above during design, and an alert on the number in it before a single customer arrived.
I did the same arithmetic properly for the real-time path, where the constraint is a synchronization budget rather than a quota — that one is written up in synchronizing one moment across every screen.
The other half of running a product alone is that nobody else is going to notice the graph. The alert is not a nice-to-have when you are the entire on-call rotation. It is the only thing standing between a busy Saturday and finding out on Monday.