This note came from a blunt question: why could two people entering a lobby create thousands of Firestore reads?
The matchmaker itself was not the problem. The server already selected a problem through compact problemIndex buckets. The noisy part was the lobby page, which was still calling a client-side problem selection hook and fetching a large batch of problems even though the lobby never displayed them.
What failed
The old lobby path mixed two jobs:
Only the first job belonged in the lobby. The second job was already handled by the backend when a match was created.
In the documented failure case, one lobby visit pulled about 500 problem documents plus notification listener reads. Two people queuing therefore had a minimum read shape of roughly 1,106 reads before refreshes, tabs, or listener reconnects made it worse.
What changed
The fix was small because the architecture already had the right server-side primitive:
useProblemSelection(true) call from the lobbyproblemIndex buckets as ID lists instead of storing full problem dataThat kept the lobby focused on queue state and pushed problem selection back to the place that had enough context to do it cheaply.
Result
The estimate fell from 1,106+ reads for a clean two-player queue path to roughly 106 reads. In the observed refresh-heavy case, the model dropped from about 7,200 reads to about 156.
The lesson is not "never use realtime listeners." The lesson is that realtime screens should subscribe to the smallest thing that can change the UI. A waiting room does not need a problem library.