Firestore cost in yen: 91% of reads came from one place, and my first cache did not work

日本語版

Development log of the Toreca Dash app / September 2026

The back end of my iPhone app Toreca Dash (a Japanese trading-card lottery and restock alert app) runs on Firebase’s Firestore database and a set of scheduled jobs. I had been reporting the cost of Gemini (AI) and of a scraping service in yen every day. I had not been reporting Firestore at all.

On the night of 4 September 2026 I converted it to yen for the first time. It was almost the same as Gemini. This is the record of how I found that 91% of the reads came from one place, why my first cache did not work, and how I fixed it again.

Numbers come from my work log of 4 September 2026. Prices are the real Tokyo-region prices.

Once it was in yen, there was a cost nobody was watching

DayReadsWritesCost per day
28 Aug2.18 million120k¥226 (about ¥6,777 a month)
1 Sep3.66 million134k¥368 (about ¥11,028 a month)
3 Sep4.21 million162k¥427 (about ¥12,810 a month)

The free tier is 50,000 reads a day. I was reading 84 times that, and it had grown 89% in a week, growing with every new article. A cost that is not shown in yen might as well not exist: about ¥13,000 a month, and nobody noticed because it was not in the report.

A meter found it in three minutes

I added a meter that counts which job reads which collection, and how much. The answer came as soon as it was in.

8,725 reads  X monitor | products   ← 91% of the 9,593 measured
  197 reads  X monitor | news_items

For each post, the X monitor asks an AI “which product is this about?”. To build the candidate list it re-read every product of the genre each time. The Pokémon genre has 693 products, so matching one post cost 693 reads. If I had searched the code, it would have taken much longer. Count before you search.

The first cache barely helped

The fix sounds simple: remember the list after reading it. I first cached for 60 seconds. It barely helped.

The X monitor handles up to 32 accounts at the same time (Promise.all). What happened was:

  1. The first call sees “not in the cache” and starts reading.
  2. Before it finishes, the other 31 calls also see “not in the cache”.
  3. All of them read the same 693 products at once.

I measured one re-read every 23 seconds. A cache remembers a finished result; callers that arrive during the read have nothing to find.

The fix is to remember the pending promise, not the result. The first call stores its promise; the other 31 wait for the same one. However many calls arrive together, there is one read.

// An illustration of the idea: cache the pending promise, not the result
cache.set(genre, { at: Date.now(), promise: loadProducts(genre) });
// callers always await cache.get(genre).promise

I added three rules with it:

Measured in a quiet period and converted to a day, 5.07 million reads became 277,000.

Another one: reading 3,085 documents every 5 minutes and using 146

After the product cache, the next item at the top of the meter was a job that rechecks Rakuten Books stock every 5 minutes with this query:

news_items where saleType in ["reservation", "guerrilla"]

It returned 3,085 documents and then filtered to Rakuten Books in code. Only 146 were relevant: 95% was wasted. At 288 runs a day that is about 888,000 reads, roughly ¥2,400 a month.

Reads fell from 888,000 to about 79,000 a day (−91%).

What I learned

A cost you do not show in yen does not exist.

This is the English version of the original Japanese article.

More in English