Skip to content
All articles

Firestore Deep Dive: Indexes, Reads/Writes, Transactions, and Pricing

Dean Jain

Dean Jain

Senior Staff Software Engineer · Enterprise AI, Data & Cloud Architect

· 6 min read

FirestoreGCPNoSQL
---
config:
  theme: dark
  fontSize: 17
  themeVariables:
    fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart TD
    C["📁 Collection"]:::gov --> D["📄 Document (≤ 1 MB)"]:::server
    D --> F["fields → values"]:::obs
    D --> M["maps (nested objects)"]:::obs
    D --> SC["📁 Subcollection"]:::gov
    SC --> D2["📄 Document"]:::server
    classDef gov fill:#E0D6F5,stroke:#9B7EDE,stroke-width:2px,color:#0F172A
    classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
    classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A

Figure 1: Firestore's data model documents (lightweight JSON-like records) live in collections, which nest via subcollections.

Firestore is easy to start with and surprisingly easy to get wrong on the bill. Two facts explain almost everything about it: every query is indexed (so query speed scales with your result size, not your data size), and you pay per document operation (so how you model data is how you control cost). Add Google’s strongly-consistent, Paxos-replicated storage underneath, and you have a database with very different instincts from a relational one. Here’s how it actually works.

Summary

  • Documents in collections. A document is a JSON-like record (≤ 1 MB) of fields and nested maps; collections contain documents; subcollections nest collections inside documents (up to 100 levels). Schemaless by design.
  • Everything is indexed. Single-field indexes are created automatically; composite indexes you create on demand. Result: query performance depends on the result set size, not the database size.
  • Strong consistency, by default. Writes go through Paxos consensus across synchronously-replicated zones/regions, so you always read the latest value at scale.
  • Data modeling = cost modeling. You’re billed per read/write/delete operation, so your collection structure and index choices directly drive your bill.
  • Tune your indexes. Exempt fields you never query, watch the 500 writes/sec ceiling on sequential indexed fields, and use index merging to cut cost.

1. The data model: documents, collections, subcollections

The unit of storage is the document a lightweight record of fields mapping to values, identified by a name, capped at 1 MB. Treat it like a JSON record: it holds strings, numbers, and complex nested objects (called maps). Documents live in collections, which are just containers you use to organize data and build queries.

Firestore is schemaless documents in the same collection can hold totally different fields but you should keep fields consistent across documents so they’re easy to query. A reference is a lightweight pointer to a location (creating one does no network I/O, whether or not data exists there).

The powerful structural tool is the subcollection: a collection attached to a specific document, following a strict alternating collection → document → collection pattern (you can’t put a collection directly in a collection, or a document in a document). Subcollections let you nest hierarchically up to 100 levels deep with one sharp edge worth tattooing on your wrist: deleting a document does not delete its subcollections. They’re orphaned, still stored, still billable.

2. Everything is indexed

Here’s Firestore’s defining design choice: it guarantees query performance by indexing every query. The payoff is huge query speed depends on the size of your result set, not the number of documents in your database. A query returning 10 docs is fast whether the collection holds a thousand or a billion. There are two index types:

  • Single-field indexes created and maintained automatically. For every non-array, non-map field, Firestore keeps two collection-scope indexes (ascending and descending); for array fields, an array-contains index.
  • Composite indexes sorted mappings across an ordered list of fields. Firestore won’t auto-create these (too many possible combinations), so when you run a query that needs one, it returns an error with a link that creates the exact index for you.

That auto-indexing is why basic queries “just work,” but it isn’t free every index costs storage and adds write amplification, which is the bridge to the two things that actually bite: consistency and cost.

3. The life of a write

A write isn’t just “save a row.” It flows through the Cloud Firestore service which runs authentication, authorization, quota checks, and security rules, and manages transactions and only then reaches the storage layer:

---
config:
  theme: dark
  fontSize: 17
  themeVariables:
    fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart LR
    APP["📱 Client"]:::agent --> SVC["🛡️ Firestore service<br/>auth · rules · quota · txns"]:::gate
    SVC --> STORE["💾 Storage layer"]:::server
    STORE --> P["🗳️ Paxos leader"]:::gov
    P --> R1["replica · zone A"]:::obs
    P --> R2["replica · zone B"]:::obs
    P --> W["witness replica"]:::neutral
    classDef agent fill:#FFE08A,stroke:#E8A33D,stroke-width:2px,color:#0F172A
    classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
    classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
    classDef gov fill:#E0D6F5,stroke:#9B7EDE,stroke-width:2px,color:#0F172A
    classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
    classDef neutral fill:#ECECEC,stroke:#8A8A8A,stroke-width:2px,color:#0F172A

Figure 2: A write passes through security rules and transactions, then replicates via Paxos across zones giving strong consistency.

Data is split into ranges, each synchronously replicated across zones. Consistency across replicas is managed by the Paxos consensus algorithm: one replica per split is elected leader and handles writes. In a multi-region setup, two regions hold full read-write replicas and a third holds a witness replica (participates in consensus but stores no full copy), so the database stays writable and readable even if an entire region is lost.

The practical upshot: strong consistency by default you always read the latest value together with atomic batch operations and real transactions, at very large scale. You also get offline support: the client SDK caches active data so the app reads, writes, and queries offline, then syncs on reconnect.

4. Pricing: data modeling is cost modeling

The mental shift from SQL: Firestore bills you primarily per operation each document read, write, and delete not per byte scanned. That single fact reshapes how you model:

  • A query that returns 1,000 documents is 1,000 reads. Fetch only what you render (shallow queries, limit, pagination cursors).
  • Realtime listeners keep apps current without re-reading the whole dataset you pay for the documents that actually change, not constant polling.
  • Indexes cost too. Every automatic index is storage plus a write on every document change. Indexing fields you never query is pure waste.

A neat optimization for read-heavy startup data: data bundles. You pre-build a static file of common query results, publish it on a CDN, and clients load it into local cache so instead of paying for a million app instances to each read the same initial 100 documents, you pay once to bundle those 100. Named queries inside the bundle then run against cache or backend transparently.

5. Index optimization (the cost levers)

Because indexes drive both performance and bill, a handful of tuning moves pay off repeatedly:

  • Exempt fields you never query. Long string blobs, fields you only store add a single-field index exemption to cut storage and write cost.
  • Mind the 500 writes/sec ceiling. Indexing a sequentially increasing field (like a timestamp) caps the collection at ~500 writes/sec. If you don’t query on it, exempt it to remove the limit.
  • TTL fields must be timestamps and are indexed by default; exempt them at high traffic to protect performance.
  • Large arrays/maps can approach the 40,000 index entries per document limit exempt them if you’re not querying on them.
  • Order composite-index fields as equalities first, then the most selective range/inequality.
  • Use index merging Firestore can merge single-field equality indexes to satisfy larger equality queries, so you avoid building (and paying for) some composite indexes.

6. Structuring relationships

Firestore gives three ways to model “a thing that has many things,” and the choice is about how that list grows:

---
config:
  theme: dark
  fontSize: 17
  themeVariables:
    fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart TD
    Q{"How does the<br/>list grow?"}:::gate
    Q -->|"small, fixed"| NEST["🧩 Nested array/map<br/>in the document"]:::obs
    Q -->|"grows over time"| SUB["📁 Subcollection"]:::server
    Q -->|"many-to-many /<br/>disparate sets"| ROOT["🗂️ Root-level collection"]:::gov
    classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
    classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
    classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
    classDef gov fill:#E0D6F5,stroke:#9B7EDE,stroke-width:2px,color:#0F172A

Figure 3: Pick the relationship model by how the data grows nested for small/fixed, subcollections for growing, root collections for many-to-many.

  • Nested data keep simple, fixed lists inside the document. Easy, but the document grows with the list and retrieval slows; not scalable.
  • Subcollections for lists that grow over time; the parent document stays small, you get full query power, and collection-group queries span them. (Just remember they aren’t deleted with the parent.)
  • Root-level collections for many-to-many relationships and disparate datasets, with powerful per-collection querying.

Why it matters: Firestore rewards you for thinking about operations and result sizes, not bytes and joins. Model so your common reads return only what you render, index only what you query, and pick a relationship structure that matches how your data grows. Get that right and you inherit Google’s strongly-consistent, globally-replicated storage with low latency and a bill that scales with usage not with surprise.

References