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.
Learning outcomes
Model listener fan-out as clients × initial result size × subsequent matching changes rather than as a zero-cost “push” feature.
Compare one broad listener with bounded/selective listeners using deterministic AtlasMart updates and explicit event counters.
Explain Standard reconnect billing with and without offline persistence and separate it from Enterprise realtime-update units.
Choose polling, Core realtime listeners, or dedicated event infrastructure based on freshness, fan-out, retention, and side-effect needs.
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.
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.
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.
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.
<!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 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
onSnapshotmetadata as MongoDB change-stream semantics.
Lab checklist
-
Run the six-item seed and open
fanout.html. - Record the initial rows for broad and seller-a queries.
-
Run
node admin-pulse.mjs; compare which mutations changed each result set. -
Call
window.cleanup()and verifyactivereturns to zero. - Reload, disable/enable network, and observe that callback/reconnect count is not a durable-event count.
- 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
-
Why is
docChanges().lengthnot a billable-read counter? - What happens to Standard billing after a reconnect when offline persistence is disabled?
- Why can a scoped listener reduce noise?
- Can listener callbacks be treated as exactly-once event delivery?
- 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
-
Get realtime updates with Cloud Firestore
— document/query listeners, initial snapshots,
docChanges(), detach/error behavior. -
Access data offline
— cache behavior,
fromCache, metadata-change events, network transitions. -
JavaScript
SnapshotMetadata—hasPendingWritesandfromCachesemantics. -
JavaScript
SnapshotListenOptions— metadata updates and listen-source options. - Understand Cloud Firestore billing — Standard listener reads, reconnect charging, index-entry reads, Rules-dependent reads, and bandwidth.
- Firestore Enterprise pricing — initial read units and separate realtime-update units.
- Enterprise Core realtime listeners — Core listener support and Pipeline listener boundary.
- Enterprise Native Core/Pipeline overview — Core realtime/offline support versus Pipeline query operations.
-
MongoDB-compatibility change streams
— separate Preview change-stream model; do not equate it with
Web SDK
onSnapshot. - Firebase release notes — current CLI/SDK versions used by this lab.