worklore.dev · paths · skills & mcp · authors

How I reduced Firestore reads from 179 to 2 per tap

I had never counted my Firestore read cost. My app asked my server "what's happening now?" after every tap, and the server answered by reading weeks of data from the database each time. Firestore bills per document read. What made me count was Panth Patel's post 0 reads to switch views: Firestore board filters in the browser: work with what is already on the device instead of asking again.

AimYear is a time tracker. An AI agent (over MCP) and a Flutter app both talk to one TypeScript server on Firestore. I made the server the only brain on purpose: the agent and the app must never disagree about how many minutes went to the aim. The app's Firestore listeners were only doorbells. "Something changed" meant "ask the server again".

My agent guessed about 40 reads per refresh. Then we measured. We patched the Firestore SDK's get() to count documents, blocked every write, and ran the server's read calls on my real account. One "now" call was 59 reads. Starting or stopping the timer triggered 179. Opening the stats tab cost 266, because it recomputed 90 days of history. Most of it was the same days, read again on every tap.

So the rule flipped. Writes still go through the server, because it owns the rules (what counts as a stop, which routines join today's plan). Reads come straight from Firestore listeners on my own documents, and the numbers are computed on the device. Now a start costs 2 reads: the session and the timer block. Switching week to month costs 0.

The hard part was not drifting apart. The server's formulas were copied to Dart. A script runs the server's own functions on shared examples and saves the answers, and a Dart test must reproduce them. Two surprises came up. The server created today's plan lazily, on the first read, so an app reading Firestore directly saw no routines. That became an explicit "open the day" write. And a cold cache says "no goal" before the server answers, which would flash the first-run screen.

A cold start still reads 132 documents, mostly 95 days of sessions. I haven't measured warm starts from the local cache yet. The new version ships with the next store release.

Check if this applies to you

Applies if: a client gets its screens from your own API, and that API reads Firestore to build them on each request.

  1. In a script with admin credentials, wrap the SDK's read calls to count documents. Count a query as max(1, docs returned) and a document get as 1. Make every write method throw, so the run stays read-only.
  2. Call the endpoints your client calls for one action (open the app, start, stop, tick, open a stats screen) on a real or realistic account. Add up the reads per action.

Has the problem: one action costs tens or hundreds of reads, mostly the same unchanged documents. Doesn't have it: each action reads about as many documents as it changed.

Reproduce this

Prerequisites: a Firestore-backed app whose server builds views on each read; Firestore security rules that let a signed-in user read their own documents; admin credentials for a counting script. Your agent will need from you: the service account path, a test or real account id, the list of endpoints each client action calls, and which writes must stay on the server. Steps:

  1. Count first (the check above), per action, before changing anything. Keep the numbers.
  2. Decide the rule and write it down where the team will see it: every write goes through the server; every read comes from listeners on the user's own documents.
  3. Subscribe once at start: one listener per document or query the screens need (bounded history, e.g. 95 days). Keep the latest copy in memory.
  4. Port each view the server computed into pure client functions over that copy.
  5. Parity: a server-side script runs the server's own functions on fixed examples and writes the answers to a fixture in the client repo. A client test must reproduce them. Rerun the script whenever a server formula changes.
  6. Find anything the server creates lazily when it is read (default documents, a day's plan). Turn it into an explicit server write that the client calls at start and when the date changes.
  7. For each write, mark the affected part of the screen as updating, and clear the mark when the listener delivers a change to it. Add a timeout for writes that change nothing the client reads.
  8. Don't treat "document missing" from an empty local cache as "missing". Wait for a server snapshot before choosing an empty-state screen.

Verify: rerun the counter. Each action now costs about as many reads as it changed, and stats or filter switches cost 0. The parity test passes. A UI test that holds back listener deliveries shows the updating mark until the delivery arrives.