All posts
Engineering/Apr 2026/7 min read

Cutting active Firestore listeners by 97% on a high-traffic shopping app

How we re-architected hot reads with batched whereIn queries and an opinionated cache layer, without rewriting the data model.

When we joined Basket, the app was a hit. Tens of thousands of daily users, sub-200ms checkouts, and a generous deals catalogue that updated in near-real time. It was also lighting Firestore on fire.

Active listeners on the deals collection were sitting in the high tens of thousands at peak. Every product page opened another listener. Every nested partner offer added two more. Costs were climbing faster than retention.

The diagnosis

We pulled a week of Performance Monitoring traces and instrumented a few critical screens with custom spans. The pattern was obvious in retrospect: dozens of components were each subscribing to small slices of the same parent document. The data shape encouraged it. The lifecycle didn't punish it. So it grew.

What we changed

Three things, in order of impact. First, we replaced N parallel listeners with a single batched whereIn query that fan-outs results through an in-memory store. Second, we tagged every read site with a feature key so we could measure, not guess. Third, we put a small cache layer in front of the read path with a TTL tuned to how stale the data is allowed to be, different per surface.

The result

Active listeners dropped by ~97% within two release cycles. Slow traces improved by 35%. Crash-free sessions stayed above 99% the entire time. And, quietly, Firestore spend went down by a meaningful amount in the next billing cycle.

What we'd do differently

We'd instrument earlier. The fix was simple once we had numbers. The hard part was admitting we needed numbers in the first place.

Want to talk about something like this?
Start a project