A routine cost review flagged one project burning several times more than anything else we ran. Low traffic, high bill.
That kind of mismatch means the shape is wrong, not the size.
The project was a near-real-time dashboard — the sort operations teams leave open all day, watching activity across a network of sites. We were using Firestore as its analytics store, and Firestore charges you for every document you read. A dashboard that refreshes every minute, in front of people all day, does a lot of reads.
- 01 Firestore bills per document read, not per byte — so on a read-heavy dashboard, the number of reads is the bill.
- 02 The fix was to make one read return many records: pack records into container documents. The bill fell by ~96%.
- 03 The obvious alternatives were wrong — BigQuery is batch, not real-time, and a SQL rewrite was too big to fund then.
- 04 The cost did not vanish, it moved: reads got cheap, writes got complicated.
The reframe: pack the reads
Firestore’s pricing is simple. You pay per document you read. Most data models map one logical record to one document, so reading a thousand records costs a thousand reads. On a dashboard, that is the whole bill.
So stop mapping one record to one document.
Group many records into a single container document — an array, sized to sit under Firestore’s document limit. One read now returns the whole group, so a thousand records come back in a handful of reads. Match the billing unit to the access unit.
Think of it as a toll road. Firestore charges a toll per trip to the warehouse. Rather than ten trips for ten boxes, put ten boxes on one pallet and make a single trip — the same boxes, a fraction of the tolls.
The mechanism
The dashboard reads a time window at a time, so the data model is shaped around windows rather than individual points.
BEFORE — one document per record (one billed read each) activity/{timestamp}-{site} -> { site, metric, value, ts } activity/{timestamp}-{site} -> { site, metric, value, ts } ... 1,000 records = 1,000 reads
AFTER — one container document per window (one read returns the batch) activity/{site}/{window} -> { site, window, points: [ { metric, value, ts }, { metric, value, ts }, ... ] } ... 1,000 records = a handful of readsThe packing factor — how many records ride in one document — is limited only by Firestore’s document size limit. On a read-heavy workload, that factor is roughly how much you cut the read bill.
Why not just move off Firestore?
Because the obvious answers were worse.
BigQuery was the obvious first suggestion, and the wrong tool. It is built for large batch analytics, not the minute-level freshness an operations dashboard needs — and moving to it was a heavy migration in its own right.
A proper SQL store was the right long-term home. But it meant a near-total rewrite of the application, and we could not fund that then.
Reshaping the data model needed neither. I built the whole thing — the write layer, the test scripts, and the migration — in about two weeks.
What it costs you
This is not free. The cost moves to the write path.
When many records share a document, you cannot simply append one. You need a layer that routes each record to the right container — using a hash and date-and-hour cut-offs — and that knows where an update lands inside an existing document. Firestore locks at the document level, so a heavily-used container becomes a bottleneck for writes.
None of that is hard. But it is yours to own — you have left Firestore’s normal read and write patterns behind.
The result
The bill dropped by around 96%, and the dashboard kept its minute-level freshness. Reads even got quicker, because each one now carries more data.
One honest caveat: this is a workaround, not a destination. SQL was still the right answer. Reshaping the data model bought the time to get there — roughly 96% cheaper each month, in two weeks, without touching the rest of the application.
The lesson applies well beyond Firestore. A managed platform’s pricing quietly tells you which operations to be careful with. Before you plan a migration, look hard at what you are actually being charged for. Sometimes the cheapest fix is to reshape your data to the platform’s billing model, not to replace the platform.