# Firestore の費用を円で出したら、読み取りの 91% が 1 か所だった：キャッシュを入れても効かなかった理由

> Firestore を初めて円で見たら、月 1.3 万円で Gemini とほぼ同額だった。読み取りの 91% は商品一覧の読み直し。60 秒キャッシュが効かなかったのは、32 件が同時に動いていたから。「読み込み中の約束」を覚えて直した記録。

- URL: https://koshinstudio.com/notes/firestore-91/
- 公開: 2026-10-17
- 書いた人: Koshin（https://koshinstudio.com/about/）
- サイト: Koshin Studio

  English version (https://koshinstudio.com/en/notes/firestore-91/)

トレカダッシュ 開発の記録 ／ 2026年9月

  iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側は、Firebase の Firestore（データベース）と、定期的に動く処理でできています。費用は、Gemini（AI）と取得サービスの分は、毎日、円で出して見ていました。ところが、Firestore の費用は、レポートに載せていませんでした。

  9 月 4 日の深夜に、はじめて円で出してみました。結果は、Gemini とほぼ同額でした。この記事は、そこから読み取りの 91% が 1 か所に集中していると分かり、キャッシュを入れても効かず、直し直した記録です。

  数字は、2026 年 9 月 4 日の作業の記録から書いています。価格は、東京リージョンの実際の単価です。

## 円で出したら、誰も見ていなかった費用が出てきた

  Firestore の読み取りを、日ごとに円にしました。

    日 | 読み取り | 書き込み | 1 日の費用

      8 月 28 日 | 218 万件 | 12 万件 | 226 円（月 6,777 円）

      9 月 1 日 | 366 万件 | 13.4 万件 | 368 円（月 11,028 円）

      9 月 3 日 | 421 万件 | 16.2 万件 | 427 円（月 12,810 円）

  無料の枠は、1 日 5 万件の読み取りまでです。その84 倍を読んでいました。しかも、1 週間で 89% 増えていて、記事が増えるほど増える形でした。

  円で出していない費用は、ないのと同じでした。月に約 1 万 3 千円かかっていたのに、レポートに載っていないというだけで、誰も気にしていませんでした。

## 計器を入れたら、3 分で分かった

  どこで読んでいるのかを知るために、「どの処理が、どのコレクション（データの置き場）を、どれだけ読んだか」を数える計器を入れました。入れたその場で、答えが出ました。

  8,725 件  X 監視 | products（商品）    ← 測定した 9,593 件の 91%
  197 件  X 監視 | news_items（記事）
  X の監視は、記事を 1 件処理するたびに、AI に「どの商品の話か」を聞きます。その候補を作るために、ジャンルの商品を、毎回、全部読み直していました。ポケモンのジャンルは商品が 693 件あります。記事 1 件の照合が、693 件の読み取りです。

  コードを読んで探していたら、もっと時間がかかったはずです。探す前に、数える。計器を 1 つ足しただけで、読み取りの 91% が 1 か所だと分かりました。

## 最初のキャッシュは、ほとんど効かなかった

  直し方は単純です。商品の一覧を、一度読んだら覚えておく（キャッシュ）。最初は 60 秒覚えさせました。ところが、ほとんど効きませんでした。

  理由は、X の監視が、最大 32 アカウントを同時に処理していることでした（Promise.all）。コードは、こう動いていました。

- 1 件目が、「キャッシュに無い」と判断して、読みに行く。

- 読み終わる前に、残りの 31 件も、同じ「キャッシュに無い」を見る。

- 全員が、同じ 693 件を、同時に読みに行く。

  実測では、23 秒に 1 回、読み直していました。キャッシュは「読み終わった結果」を覚えるもので、読んでいる最中の人は、まだ何も覚えていないのです。

  直し方は、結果ではなく、「読み込み中」という約束（Promise）そのものを覚えることでした。1 件目が読み始めたら、その約束を置いておく。残りの 31 件は、新しく読みに行かず、同じ約束を待つ。同時に呼ばれても、読み込みは 1 本になります。

  // 考え方を書いた例です。結果ではなく、読み込み中の約束を覚える
cache.set(genre, { at: Date.now(), promise: loadProducts(genre) });
// 呼び出しは、いつも cache.get(genre).promise を待つ
  これに合わせて、3 つのことも決めました。

- 商品を作ったときと消したときは、その場でキャッシュを捨てる。捨てないと、もう無い商品を候補に見せたり、同じ商品をもう 1 件作ってしまったりします。

- 同じ相手を読む処理が、もう 1 つあった。別の関数が、同じ条件で商品を読んでいました。キャッシュを一本化して、片方だけ効かない状態を避けました。

- 覚える時間は、ジャンルごとに 10 分にしました。

  静かな時間帯で測ると、1 日に換算して、507 万件が 27.7 万件になりました。

## もう 1 つ：5 分おきに 3,085 件読んで、使うのは 146 件だった

  商品のキャッシュを入れたあと、計器の一番上に出てきたのが、別の処理でした。楽天ブックスの在庫を確かめ直す処理が、5 分おきに、こういう問い合わせをしていました。

  news_items where saleType in ["reservation", "guerrilla"]
  返ってきた 3,085 件を、プログラムの側で「楽天ブックスのものだけ」に絞っていました。実際の対象は 146 件で、95% が無駄です。1 日 288 回なので、約 88.8 万件、月に約 2,400 円でした。

  直し方は、2 つです。

- 対象の一覧（参照と ISBN だけ）を、2 時間覚えておく。

- ただし、判断に使う中身は、毎回読み直す。古い値で書き換える事故を作らないためです。読むのは 146 件だけなので、安く済みます。

  読み取りは、1 日 88.8 万件から約 7.9 万件へ、91% 減りました。

## 分かったこと

> 円で出していない費用は、存在しないのと同じ。

- まず、計器を直す。見ていない費用は、増え続けても気づけません。

- 探す前に数える。「ジョブ × コレクション」の集計を 1 つ足しただけで、91% の所在が 3 分で分かりました。

- 同時に動く処理に、キャッシュを付けるなら、「読み込み中の約束」を覚える。結果だけを覚えるキャッシュは、同時に来た全員を素通りさせます。

- 絞り込みは、読む前にする。全部読んでから 5% に絞るのは、95% を捨てるために読んでいるのと同じです。

- 確定した数字は、翌日の記録で見る。この日の「507 万件が 27.7 万件」は、静かな時間帯の測定でした。

  この話に出てくるもの

- トレカダッシュ (https://koshinstudio.com/works/torecadash/) — トレカの抽選・予約・先着の情報を集める iPhone アプリ

- 「しょうがない」を測り直したら、取得の費用は8分の1だった (https://koshinstudio.com/notes/scrape-cost/) — 同じアプリの、別の費用の記録

- 「減るはず」が、二度外れた (https://koshinstudio.com/notes/torecadash-ai/) — AI への問い合わせを減らそうとして、数え直した記録
