Chapter 08 · Offline Persistence, Local Cache, Synchronization, Conflict Behavior, and Offline Indexes

Local Query Indexes, Cache Growth, User Sign-Out / Data Clearing, and Offline Performance

Use Firestore persistent-cache local query indexes, measure offline query performance, manage cache growth and isolate emulator tests.

Intermediate120–145 minutesLocal offline query indexesFirebase JS 12.19.0 · CLI 15.30.0Last reviewed: September 2026

Learning outcomes

01

Explain why an offline query can become slow as persistent cache grows even when the equivalent server query is well indexed.

02

Enable automatic persistent-cache query indexing and measure local query time without inventing benchmark numbers.

03

Separate local query indexes from server single-field/composite/vector indexes and from Enterprise scan/index behavior.

04

Design cache growth, sign-out/reset and local-index lifecycle policies for realistic mobile/web use.

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 08 reproducibility baseline · reviewed 16 September 2026

AtlasMart continues the same environment used in Chapters 01–07: project ID demo-atlasmart-firestore, Standard edition / Native mode / (default) database for the mandatory 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+. The emulator has no production location or billing contract; any production region, pricing, SLA, IAM and managed-service latency remain explicitly unproven by this lab.

Evidence boundary

The mandatory exercises run locally and deliberately manipulate client network state. They prove client-cache, queued-write, listener, Rules and synchronization behavior for the pinned SDK/emulator combination. They do not prove production WAN p95/p99 latency, device-OS background behavior, billing, data-recovery guarantees, real multi-region propagation, every browser storage policy, or Enterprise Pipeline behavior. Pipeline operations do not support offline persistence; Enterprise offline exercises must use Core operations.

1. AtlasMart problem: the catalog is cached, but offline filtering gets slower

A field salesperson uses AtlasMart offline for days. The device accumulates hundreds or thousands of cached catalog documents. By default, the Firestore SDK can scan cached documents to answer an offline query. That is correct but may become slower as the cache grows. Persistent cache supports local query indexes so the SDK can accelerate repeated offline queries.

Do not confuse two index planes

Server indexes determine backend query execution and are configured/deployed with Firestore. Persistent-cache indexes are client-local structures used only for local query execution. Creating a local index does not satisfy a missing production composite index, change server billing, or prove Enterprise query efficiency.

2. Automatic local indexing is disabled by default

With persistent cache enabled, obtain the SDK’s persistent cache index manager. Automatic index creation must be enabled each app start. The SDK then decides which cached collections/queries justify local indexes. Manual setIndexConfiguration() APIs are obsolete; prefer automatic index creation for current code.

measure local query before/after auto-indexing
import {  getPersistentCacheIndexManager,  enablePersistentCacheIndexAutoCreation,  disablePersistentCacheIndexAutoCreation,  deleteAllPersistentCacheIndexes,  getDocs, query, collection, where, orderBy, limit} from "https://www.gstatic.com/firebasejs/12.19.0/firebase-firestore.js";const manager=getPersistentCacheIndexManager(db);if(!manager) throw new Error("Persistent cache is required for local query indexes");const q=query(collection(db,"catalogItems"),  where("category","==","camera"),orderBy("stock","asc"),limit(50));async function timed(label){  const t0=performance.now();  const snap=await getDocs(q);  const ms=performance.now()-t0;  console.log(label,{ms,count:snap.size,fromCache:snap.metadata.fromCache});}await disableNetwork(db);await timed("offline-before-auto-index");enablePersistentCacheIndexAutoCreation(manager); // enable on every app start// Run the query repeatedly / allow the SDK to decide when indexing is useful.for(let i=0;i<8;i++) await timed(`offline-after-enable-${i}`);// For a reset experiment only:// disablePersistentCacheIndexAutoCreation(manager);// deleteAllPersistentCacheIndexes(manager);

Do not copy a single elapsed-time number into architecture documentation. Record fixture size, browser/device, warm/cold state, number of repetitions and cache mode. The first runs may include setup/index-building work; later runs may differ.

3. Build enough cached data to make the mechanism visible

seed-offline.mjs
import { initializeApp } from "firebase-admin/app";import { getFirestore } from "firebase-admin/firestore";process.env.FIRESTORE_EMULATOR_HOST="127.0.0.1:8080";process.env.GCLOUD_PROJECT="demo-atlasmart-firestore";initializeApp({projectId:"demo-atlasmart-firestore"});const db=getFirestore();const writer=db.bulkWriter();for(let i=0;i<1500;i++){  const id=`offline-${String(i).padStart(4,"0")}`;  writer.set(db.collection("catalogItems").doc(id),{    sellerId:`seller-${i%20}`,    category:["camera","hub","cable","sensor"][i%4],    stock:(i*17)%101,    priceCents:1000+(i%250)*37,    offlineFixture:true  });}await writer.close();console.log("seeded 1500 deterministic offline fixtures");

Seed on the emulator, then while online fetch the fixture collection so documents enter the persistent cache. Disconnect the SDK network and run the filtered/ordered query. The experiment is local and free; it does not claim production query latency or billing.

4. Cache growth is bounded by a garbage-collection target, not an exact limit

persistent cache sizing
const db=initializeFirestore(app, {  localCache: persistentLocalCache({    cacheSizeBytes: 80 * 1024 * 1024,    tabManager: persistentMultipleTabManager()  })});// The threshold is approximate; cleanup is attempted after exceeding it.// CACHE_SIZE_UNLIMITED disables garbage collection and should be a deliberate choice.
Control Effect Caution
cache-size threshold SDK attempts LRU cleanup after cache exceeds target not a hard maximum
automatic local indexing can speed offline queries over large cached collections uses device storage/CPU; enable per app start
delete local indexes resets local query-index state queries fall back to scanning cached docs until indexes are rebuilt
clear persistence removes cached documents and pending writes for test/reset must be done with Firestore stopped; not secure erase

5. Sign-out and emulator reset need a cache plan

The Firestore emulator clears backend contents when it shuts down, but client offline cache is not automatically cleared. If a test restarts the emulator and then sees old data from persistent cache, that is a test-isolation bug—not proof that the emulator retained data. Either disable persistence for tests that do not test offline behavior, or terminate/clear the client cache between scenarios.

6. Wrong approach: “enable every local index and disable cache cleanup”

Local indexes are a performance tool with device resource cost. Unlimited cache plus aggressive indexing can increase storage footprint and privacy exposure. Start from measured offline UX: how many cached documents, which queries, which devices, and how long users remain offline. Let the SDK’s automatic indexing decide when indexes are useful and retain an explicit reset path for tests.

Production judgment

Benchmark local query performance separately from server query performance. Track cache mode, fixture cardinality and device class. A p95/p99 local-query target is meaningful only with controlled device/cache conditions. Do not infer server read cost from local query time, and do not infer local speed from server Query Explain. Lesson 5 combines cache, restart, conflict, Rules changes and reconnect into one end-to-end failure test.

Knowledge check

  1. Are persistent-cache query indexes the same as Firestore server indexes?
  2. Is automatic local index creation enabled automatically?
  3. Why seed many documents in the emulator?
  4. Does the cache-size threshold guarantee the cache never exceeds that size?
  5. Why can emulator tests show stale data after backend reset?
Review the answers

1. No. They are client-local structures for offline/local query execution.

2. No. It is disabled by default and must be enabled each app start.

3. To make local scan/index behavior observable without billed cloud traffic.

4. No. It is an approximate target that triggers cleanup attempts.

5. Persistent client cache survives independently unless the test disables or clears it.

Summary and next step

Persistent local indexes optimize the client cache, not the server. Lesson 5 now runs the complete offline failure matrix, including a Rules change while a write is pending.

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.