Chapter 07 · Realtime Listeners: Snapshots, Change Events, Metadata, Latency, and Listener Lifecycle

Listener Fan-Out at Scale, Query Selectivity, Reconnects, Resume Behavior, and Bandwidth / Read Cost

Reason about Firestore listener fan-out, query selectivity, reconnect behavior, Standard versus Enterprise realtime billing, bandwidth, and when realtime is the wrong tool.

Intermediate125–150 minutesFan-out + reconnect costFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Model listener fan-out as clients × initial result size × subsequent matching changes rather than as a zero-cost “push” feature.

02

Compare one broad listener with bounded/selective listeners using deterministic AtlasMart updates and explicit event counters.

03

Explain Standard reconnect billing with and without offline persistence and separate it from Enterprise realtime-update units.

04

Choose polling, Core realtime listeners, or dedicated event infrastructure based on freshness, fan-out, retention, and side-effect needs.

Execution and safety note

Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.

Chapter 07 reproducibility baseline · reviewed 15 September 2026

AtlasMart continues the same environment used in Chapters 01–06: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database for the main lab, Firestore emulator 127.0.0.1:8080, Authentication emulator 127.0.0.1:9099, Emulator UI 127.0.0.1:4000, Firebase CLI 15.30.0, Firebase JavaScript SDK 12.19.0, Firebase Admin Node SDK 14.4.0 (bundling @google-cloud/firestore 9.1.0), and Node.js 22+. Browser examples import the pinned Web SDK from Google-hosted modules and connect only to local emulators.

Evidence boundary

The authoring environment did not execute a billed Firestore database or collect production listener telemetry. Emulator steps below are deterministic correctness exercises; shown event sequences are expected invariants, not captured benchmark evidence. The emulator cannot prove production billing, WAN reconnect behavior, managed index enforcement, production latency percentiles, or all mobile background/OS lifecycle behavior.

1. AtlasMart problem: 100,000 dashboards make one product update expensive

A single Firestore document update is cheap to imagine from the writer's viewpoint. Realtime cost is driven by the read side too. If many active listeners include that document in their result set, the same logical update can fan out to many clients. Firestore scales the infrastructure, but your application still owns query selectivity, result size, client count, reconnection pattern, and bandwidth.

design model · not a billing invoice
listener_surface ≈ active_clients × matching_result_set + matching_changes_delivered_over_timeFor Standard:  initial sync -> document reads (+ applicable index-entry reads)  add/update -> document read per affected result document  removed because data changed -> document read  document deleted -> no listener read charge for that deletion eventFor Enterprise:  initial query sync -> read units  subsequent realtime changes -> realtime-update units (size-sensitive)Always use current regional pricing and actual usage telemetry for money.

2. Broad versus scoped query experiment

Use two listeners against the same six-item fixture. The broad listener tracks the four lowest-stock products globally. The scoped listener tracks up to four low-stock products for seller-a. Run the deterministic Admin pulse that modifies two seller-a and two seller-b documents. Instrument snapshot count, docChanges() count, approximate JSON payload bytes, and active listener count.

fanout.html · event instrumentation
<!doctype html><meta charset="utf-8"><pre id="out"></pre><script type="module">import { initializeApp } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-app.js";import { getAuth, connectAuthEmulator, signInAnonymously } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-auth.js";import {  getFirestore, connectFirestoreEmulator, collection, doc, query, where, orderBy, limit,  onSnapshot, setDoc, updateDoc, serverTimestamp, disableNetwork, enableNetwork} from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";const app = initializeApp({ projectId:"demo-atlasmart-firestore", apiKey:"demo-key", appId:"demo-app" });const db = getFirestore(app); connectFirestoreEmulator(db, "127.0.0.1", 8080);const auth = getAuth(app); connectAuthEmulator(auth, "http://127.0.0.1:9099", { disableWarnings:true });const credential = await signInAnonymously(auth);const uid = credential.user.uid;console.log("uid", uid);const out=document.querySelector('#out');const stats={active:0,snapshots:0,changes:0,payloadBytes:0};const enc=new TextEncoder();function watch(name,q){  stats.active++;  const stop=onSnapshot(q, {includeMetadataChanges:true}, snap=>{    const rows=snap.docs.map(d=>({id:d.id,...d.data()}));    const changes=snap.docChanges().map(c=>`${c.type}:${c.doc.id}`);    stats.snapshots++; stats.changes+=changes.length;    stats.payloadBytes+=enc.encode(JSON.stringify(rows)).byteLength;    out.textContent += JSON.stringify({name,meta:snap.metadata,changes,stats:{...stats}})+"\n";  });  return ()=>{stop();stats.active--;};}const broad=query(collection(db,'catalogItems'),orderBy('stock','asc'),limit(4));const scoped=query(collection(db,'catalogItems'),where('sellerId','==','seller-a'),orderBy('stock','asc'),limit(4));const stopBroad=watch('broad',broad), stopScoped=watch('seller-a',scoped);window.cleanup=()=>{stopBroad();stopScoped();};</script>

The byte counter is an application-side approximation of JSON you chose to serialize—not Firestore's network-billing byte count. The change counter is not a billing counter either. Its purpose is comparative: which query causes the screen to receive irrelevant changes under the same mutation workload?

3. Reconnects and resume behavior

SDK listeners can reconnect automatically after transient connectivity loss. Treat this as state synchronization, not exactly-once event replay. The service/SDK may use internal resume mechanisms, but your application contract remains “eventually regain a current query snapshot or surface an error.” Do not persist callback sequence numbers as if they were durable event offsets.

For Standard mobile/web billing, current documentation distinguishes persistence state: with offline persistence enabled, a disconnect longer than 30 minutes is charged like a new query; with offline persistence disabled, a disconnect/reconnect is charged like a new query each time. Query index-entry charging rules can also apply. Rules that use dependent document reads can incur extra reads when the query is issued, updated, reconnects, Rules change, or dependent documents change.

Enterprise is a different meter.

Enterprise bills initial synchronization in read units and subsequent realtime changes in realtime-update units; update-unit calculation is size-sensitive. Do not apply Standard “one document read per realtime update” budgeting to Enterprise. Re-check current region/edition pricing before deployment.

4. Bound the result set before tuning listeners

Design Freshness Fan-out/cost surface Failure mode
Unbounded collection listener High Grows with entire collection and all matching churn Memory/bandwidth/read growth; noisy UI
Bounded query listener High Initial bounded set + changes crossing/within boundary Top-N churn may replace rows frequently
Polling one-shot query Periodic Requests at explicit intervals Stale between polls; repeated initial reads
Dedicated event system Event-oriented Designed for durable delivery/consumer semantics More infrastructure; separate state read still needed

Use realtime for the part of the screen that truly needs realtime. A long order-history list can load with cursor pagination while only the current order status has a document listener. “Everything live” is rarely a requirement.

5. Standard vs Enterprise vs Pipeline vs MongoDB compatibility

  • Standard Native/Core: realtime listeners are built in; indexes are required for queries.
  • Enterprise Native/Core: realtime listeners and offline persistence are available, but optional indexing and Enterprise billing change performance/cost reasoning.
  • Enterprise Native/Pipeline: Pipeline operations do not support realtime listeners; use Core operations when realtime/offline behavior is required.
  • MongoDB compatibility: change streams are a distinct API/operational model and are currently Preview; do not teach onSnapshot metadata as MongoDB change-stream semantics.

Lab checklist

  1. Run the six-item seed and open fanout.html.
  2. Record the initial rows for broad and seller-a queries.
  3. Run node admin-pulse.mjs; compare which mutations changed each result set.
  4. Call window.cleanup() and verify active returns to zero.
  5. Reload, disable/enable network, and observe that callback/reconnect count is not a durable-event count.
  6. Document a production measurement plan: active listeners, result-set sizes, delivered bytes, reconnect rate, Standard read/index-entry metrics or Enterprise units, and p95/p99 update-to-render latency.

Production judgment

Listener scalability is usually a product/query-shape problem before it is an SDK problem. Keep live result sets selective, detach them when hidden, separate durable events from synchronized UI state, and budget reconnects. For extreme broadcast fan-out, consider whether a purpose-built messaging/event channel plus occasional state reads fits better.

Knowledge check

  1. Why is docChanges().length not a billable-read counter?
  2. What happens to Standard billing after a reconnect when offline persistence is disabled?
  3. Why can a scoped listener reduce noise?
  4. Can listener callbacks be treated as exactly-once event delivery?
  5. Why separate Standard and Enterprise cost models?
Review the answers

1. It is a client change representation; billing also depends on initial reads, reconnects, index-entry reads, Rules-dependent reads, edition, and pricing semantics.

2. The listener is charged as a new query on disconnect/reconnect according to current pricing guidance.

3. Changes to documents outside the query result do not need to update that listener, reducing result churn and usually initial/result-set size.

4. No. They represent synchronized state and can be re-observed across local writes, reconnects, remounts, and metadata transitions.

5. Standard realtime changes are document-read based; Enterprise separates initial read units from realtime-update units and uses different unit semantics.

Summary and next step

You now have a measurable listener surface. Lesson 4 makes ownership explicit in framework/component lifecycle so the application cannot accidentally keep old screens subscribed forever.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.