Chapter 15 · Scaling and Hotspots: Key Distribution, Index Fan-Out, Sequential Values, and Ramp-Up
Sequential Document IDs / Timestamps, Monotonic Fields, High Write Rates, and Index Hotspots
Show how sequential document IDs and monotonically indexed fields create moving hotspots, then redesign AtlasMart event keys and timestamp indexing deliberately.
1. Why a timestamp can hotspot even when document IDs are random
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 wisely stopped using event-000001,
event-000002, … as document IDs and switched to
Firestore auto IDs. Yet write latency rises as event volume
grows. The overlooked field is ingestedAt: every
new event receives a later timestamp, and Standard Native
automatically indexes that field. The document keys are
scattered, but the timestamp index entries arrive at one moving
edge of an ordered index.
This is the key distinction: document-key hotspot and index-key hotspot are independent. Random IDs fix the first. A sequential indexed field can still create the second.
AtlasMart continues the same mandatory environment used in
Chapters 01–14: project ID
demo-atlasmart-firestore, Standard edition /
Native mode / (default) database, 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.js SDK
14.4.0 carrying
@google-cloud/firestore 9.1.0,
@firebase/rules-unit-testing 5.0.2, and Node.js
22+. Mandatory benchmarks remain emulator-only and no-cost.
They teach measurement mechanics and relative shapes; they do
not certify production throughput, split behavior,
Key Visualizer patterns, billing, or regional latency.
Firebase JavaScript SDK 12.19.0 was released 9
September 2026. Firebase Admin Node.js 14.4.0 was
released 10 September 2026 and uses
@google-cloud/firestore 9.1.0. Firebase CLI
15.30.0 was released 9 September 2026. Standard
Native documentation still describes 500/50/5 gradual warm-up
and the 500 writes/s constraint for a collection with a
monotonically changing indexed field. Enterprise
Native indexes are optional rather than automatic; unindexed
queries can scan, so index and cost reasoning is different
even though document/key-range hotspots still exist.
Learning outcomes
Explain why lexicographically sequential document IDs concentrate document-key traffic even when each document is independent.
Explain why monotonically changing indexed timestamps or sequence numbers can create a moving index hotspot in Standard Native.
Apply the documented 500 writes/s sequential-index constraint only where its conditions actually hold.
Choose among index exemption, timestamp sharding and query redesign based on a query inventory rather than blindly removing indexes.
Separate Standard automatic indexes from Enterprise optional indexes and MongoDB-compatibility indexing.
2. Two moving edges: document IDs and indexed values
| Pattern | Key space receiving new writes | Risk | Safer shape |
|---|---|---|---|
| Sequential document IDs | Document-key range near newest ID | Moving document hotspot | Auto/scatter IDs or application-generated random IDs |
| Random IDs + indexed sequential timestamp | Index-key range near newest timestamp | Moving index hotspot | Exempt timestamp if not queried, or shard required timestamp index |
| Random IDs + non-indexed sequential timestamp | Document keys remain scattered; no timestamp index entries | Timestamp unavailable to Standard query ordering/filtering | Use another query shape/materialized view if needed |
| Random IDs + required sharded timestamp | Multiple index prefixes share the moving edge | More read complexity but more write distribution | Query each shard and merge bounded results |
Standard Native documentation states that when a collection contains a monotonically increasing or decreasing indexed field, the maximum write rate to that collection is 500 writes per second. This is not “all timestamp fields” and it is not the 500/50/5 ramp guideline. If the timestamp is not indexed, that specific index constraint does not apply; if the field is required for a query, simply disabling the index breaks the access pattern.
3. Start with the Chapter 5 query contracts
AtlasMart already treats queries as named contracts. Before
changing indexing, add an inventory entry for every event query
that consumes occurredAt or
ingestedAt. A field can be safely exempted only
when no required Standard query depends on it.
{ "indexes": [ { "collectionGroup": "events", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "tenantId", "order": "ASCENDING" }, { "fieldPath": "kind", "order": "ASCENDING" }, { "fieldPath": "occurredAt", "order": "DESCENDING" } ] } ], "fieldOverrides": [ { "collectionGroup": "events", "fieldPath": "ingestedAt", "indexes": [] }, { "collectionGroup": "events", "fieldPath": "rawPayload", "indexes": [] }, { "collectionGroup": "events", "fieldPath": "debugAttributes", "indexes": [] } ]}
ingestedAt but keeps
occurredAt
ingestedAt exists for operations/audit sequencing
and is not a user query key in this model.
occurredAt is part of the product query contract,
so its composite index remains. If
occurredAt itself reaches a sequential-index
throughput problem, the model must change rather than silently
remove the index.
4. When a required timestamp query must scale beyond one ordered index edge
The sharded timestamp pattern deliberately adds a small random
shard value and indexes
(timestampShard, occurredAt) instead of relying on
one globally ordered timestamp edge. Writes are spread across
shard-prefixed index ranges. Reads query each shard and merge
the top results in application code. This trades write
scalability for read amplification and application complexity;
choose a shard count from measured workload, not folklore.
import { randomInt } from "node:crypto";import { Timestamp } from "firebase-admin/firestore";export async function writeEvent(db, event) { const shard = randomInt(0, 16); const occurredAt = Timestamp.fromDate(event.occurredAt); const ref = db.collection("events").doc(); // Firestore-style scatter ID await ref.set({ tenantId: event.tenantId, kind: event.kind, occurredAt, timestampShard: shard, ingestedAt: Timestamp.now(), payload: event.payload }); return ref.id;}// If a required query must order by a sequential timestamp at a write rate// beyond Standard's sequential-index constraint, query by shard + timestamp// and merge a bounded number of shard result sets in application code.
The query side must issue one bounded query per shard, preserve
the same filters, then merge-sort by occurredAt.
Pagination becomes a multi-cursor problem because each shard has
its own continuation state. That complexity is the cost of
distributing the ordered index.
5. Deliberately wrong approach: encode time in the document ID
20260917T010000Z-000001 looks operationally
convenient because a console listing appears chronologically
ordered. At high write rates it puts new document keys next to
each other. Worse, if ingestedAt is also indexed,
the same write can create both a document-key moving hotspot and
an index-key moving hotspot.
The repair is to keep identity independent of sort order. Use scattered document IDs. Store time as data. Exempt unused sequential time fields in Standard, or shard the time index only when a required access pattern justifies the added complexity.
6. Edition-aware decision table
| Decision | Standard Native | Enterprise Native | MongoDB compatibility |
|---|---|---|---|
| Automatic single-field index on timestamp | Yes unless exempted | No; indexes are optional | Use compatibility-mode index semantics |
| Unindexed query behavior | Required query generally fails and points to index creation | May scan instead of failing; can become slower/costlier | Mode-specific query planner behavior |
| Sequential index hotspot | Relevant when sequential field is indexed; documented 500 writes/s constraint | Created sequential indexes can still concentrate index writes; optional indexing changes the starting point | Consult mode-specific scale/index docs |
| Random document IDs | Still recommended for high-rate key distribution | Still recommended | Avoid narrow/sequential hot keys |
7. Observable lab: compare key-generation shapes, not production capacity
{ "name": "atlasmart-firestore-ch15", "private": true, "type": "module", "engines": { "node": ">=22" }, "dependencies": { "firebase-admin": "14.4.0" }, "devDependencies": { "firebase-tools": "15.30.0" }, "scripts": { "emulators": "firebase emulators:start --only firestore --project demo-atlasmart-firestore", "bench": "node bench-writes.mjs", "reset": "node reset-ch15.mjs" }}
process.env.FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080";process.env.GCLOUD_PROJECT = "demo-atlasmart-firestore";import { initializeApp } from "firebase-admin/app";import { getFirestore, FieldValue } from "firebase-admin/firestore";import { randomUUID } from "node:crypto";initializeApp({ projectId: "demo-atlasmart-firestore" });const db = getFirestore();const percentile = (sorted, p) => { if (!sorted.length) return null; const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1); return sorted[index];};async function runStage({ mode, writes, concurrency }) { const latencies = []; let next = 0; let errors = 0; async function worker(workerId) { while (true) { const i = next++; if (i >= writes) return; const started = performance.now(); try { if (mode === "hot-document") { await db.doc("benchmarks/ch15-hot").set({ n: FieldValue.increment(1), updatedAt: FieldValue.serverTimestamp() }, { merge: true }); } else { const id = mode === "sequential" ? `evt-${String(i).padStart(10, "0")}` : `evt-${randomUUID()}`; await db.doc(`benchmarks/ch15-${mode}/events/${id}`).set({ mode, ordinal: i, observedAt: new Date().toISOString(), workerId }); } } catch (error) { errors++; console.error(JSON.stringify({ mode, i, code: error.code, message: error.message })); } finally { latencies.push(performance.now() - started); } } } const t0 = performance.now(); await Promise.all(Array.from({ length: concurrency }, (_, i) => worker(i))); const elapsedMs = performance.now() - t0; latencies.sort((a, b) => a - b); return { mode, writes, concurrency, elapsedMs, observedOpsPerSecond: writes / (elapsedMs / 1000), errors, latencyMs: { p50: percentile(latencies, 50), p95: percentile(latencies, 95), p99: percentile(latencies, 99), max: latencies.at(-1) } };}for (const stage of [ { mode: "random", writes: 600, concurrency: 12 }, { mode: "sequential", writes: 600, concurrency: 12 }, { mode: "hot-document", writes: 250, concurrency: 12 }]) { console.log(JSON.stringify(await runStage(stage)));}
Run the random and sequential stages several times after resetting emulator state. Preserve every JSON record. The lesson goal is to make the distinction between identifier strategies explicit and to verify that your harness records tail latency and errors. If one local mode looks faster, do not generalize it into a production claim; the emulator does not model managed key-range splitting.
8. Verification and rollback
- No product query relies on a field before you exempt its index.
- Sequential IDs are removed from high-rate event writes.
- If timestamp sharding is introduced, the shard count is configuration with a migration plan, not a magic constant.
- Pagination tests cover all shard cursors and duplicate/tie handling.
- The old query/index path remains available until new query-contract tests pass.
Production judgment and bridge to Lesson 3
Random IDs solve document-key distribution; index design solves index-key distribution. Both must match the query contract. Lesson 3 adds the time dimension: even a good key/index design can suffer if a new collection or migration receives full production traffic before the storage layer has time to adapt.
Knowledge check
- Does every timestamp field impose a 500 writes/s limit in Standard Native?
- Why not sort by timestamp encoded in the document ID?
- When is an index exemption safe?
- What does timestamp sharding trade?
- How does Enterprise change the problem?
Review the answers
1. No. The documented constraint applies to a collection with a monotonically changing field when that field is indexed.
2. High-rate sequential IDs can concentrate new document writes into a moving key range; identity and ordering should be modeled separately.
3. Only when the query inventory proves no required query depends on that field or a replacement access path is already tested.
4. It distributes index writes at the cost of multiple bounded reads, merge logic, more complex pagination and potentially more read cost.
5. Indexes are optional rather than automatic, so you decide which ordered structures to create; document hotspots remain and unindexed scans can be expensive.
Summary
AtlasMart now treats document IDs and indexed field values as separate key spaces. Scattered IDs prevent one class of moving hotspot; disciplined indexing or sharded required indexes address another.
Authoritative references
- Understand reads and writes at scale
- Firestore best practices
- Standard index overview and indexing best practices
- Sharded timestamps
- Key Visualizer overview
- Key Visualizer document-key patterns
- Enterprise Native Core/Pipeline overview
- Enterprise Native index overview
- Enterprise latency troubleshooting
- Firestore quotas and limits
- Firebase current releases
- Firebase Admin Node.js SDK release notes