Chapter 21 · TTL, Retention, Expiration, Backfills, and Lifecycle Automation
TTL Policies and Expiration Fields: Asynchronous Deletion Semantics and Non-Instant Guarantees
Understand TTL as asynchronous lifecycle cleanup, not an exact-time scheduler: eligibility, delayed deletion, latest-field semantics, query visibility, subcollections, listeners, functions, billing, and edition differences.
1. AtlasMart problem: an expired session is still visible
AtlasMart wants abandoned sessions and transient event documents
removed automatically. An engineer sets
expireAt and assumes a document becomes unreadable
at that exact instant. That assumption is wrong. TTL decides
when a document becomes eligible for managed deletion;
physical deletion is asynchronous, normally completed within
about 24 hours, and an expired document can still appear in a
query or direct lookup until the sweeper removes it. Product
access and storage cleanup are therefore separate mechanisms.
- Distinguish expiration eligibility from physical deletion and product visibility.
- Explain the current Standard, Enterprise, and MongoDB-compatible TTL value/configuration differences.
- Predict how queries, listeners, Cloud Functions, subcollections, and a changed TTL field behave around expiration.
- Build a deterministic local lifecycle harness without pretending the emulator runs the production TTL sweeper.
- Design exact-time behavior with explicit application logic instead of TTL timing assumptions.
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 keeps the course-wide project identity
demo-atlasmart-firestore. Mandatory work is local
and no-cost: Node.js 22+, Firebase CLI 15.30.0,
Firestore emulator 127.0.0.1:8080, Auth emulator
127.0.0.1:9099, Emulator UI
127.0.0.1:4000, and the same Standard-edition
Native-mode mental model used by earlier chapters. The default
database is (default). TTL sweeping itself is
not treated as an emulator guarantee: the lab
models eligibility and lifecycle rules deterministically,
while an optional real-project check verifies actual managed
TTL deletion only in an isolated billed project. TTL deletes
are excluded from Firestore's free usage and require billing.
Expiration is a business fact; TTL deletion is an
asynchronous storage-cleanup mechanism.
A document whose expireAt is in the past may
still exist and be returned by queries until the managed
sweeper deletes it. Therefore product behavior must filter or
reject expired state explicitly when timeliness matters. TTL
is not a scheduler, not a transaction boundary, not a
legal-hold engine, and not a substitute for backups/PITR.
2. Edition and mode boundary
TTL is available across current Firestore offerings, but configuration and billing surfaces differ. Standard Native uses a TTL policy on a field in a collection group. Enterprise Native and MongoDB-compatible documentation describe TTL index behavior and managed-delete billing. Enterprise can accept a Date/time value or an array containing Date/time values; Standard requires Date/time. MongoDB compatibility exposes familiar TTL-index syntax, but it is still Firestore-managed asynchronous deletion—not a self-managed MongoDB background thread.
| Dimension | Standard Native | Enterprise Native | MongoDB compatibility |
|---|---|---|---|
| TTL configuration | TTL policy on one field per collection group; Standard value is Date/time | TTL policy/index semantics; Enterprise may also accept arrays containing Date/time values |
TTL index per collection; MongoDB API supports
expireAfterSeconds
|
| Indexing | Automatic single-field indexes by default; TTL field can be exempted | Indexes are optional by default; TTL index is not a query-performance index | TTL index is distinct from query-planning indexes |
| Billing | TTL deletes are billed document deletes and have no free usage | TTL uses managed delete units | TTL uses managed delete units |
| Managed delay | Typically deleted within 24 hours after expiration; not ordered or transactional | Same asynchronous retention principle | Same asynchronous retention principle |
| Local emulator | Useful for document/rules/application lifecycle tests; do not claim it reproduces managed TTL sweeping | Enterprise-specific managed behavior must be verified separately | No Local Emulator Suite MongoDB wire-compatible TTL sweeper |
3. The state machine: LIVE → EXPIRED-ELIGIBLE → PHYSICALLY-DELETED
Keep three timestamps separate:
businessExpiresAt or the policy-derived expiration,
the instant the document becomes eligible, and the eventual
managed deletion time. The middle state matters because reads
can still return the document. If the application promises “a
session is invalid after 30 minutes,” enforcement must compare
the business clock on read/action. Waiting for a TTL delete
would make authorization depend on an intentionally non-instant
background process.
| State | Document exists? | Should product treat it active? | TTL action |
|---|---|---|---|
| LIVE | Yes | Usually yes | None |
| EXPIRED-ELIGIBLE | Yes, possibly for hours | No if policy says expired | Queued/eligible for low-priority managed deletion |
| PHYSICALLY-DELETED | No | No | Complete; listeners/functions see deletion event where configured |
4. Exact deletion semantics that change designs
- Expired documents continue to appear in queries and lookups until deletion occurs.
- Applying a new TTL policy to existing data can create a large backlog of already-expired documents.
- TTL deletion order is not guaranteed to match expiration-time order.
- Documents with the same expiration timestamp are not deleted atomically or necessarily together.
- Deleting a parent through TTL does not cascade to subcollections.
- If an expired-but-not-yet-deleted document gets a later expiration value, Firestore honors the latest value and it can become non-expired.
- TTL-driven deletion notifies active snapshot listeners and triggers Firestore delete-trigger functions.
5. Mandatory local lab: model eligibility and product visibility
Seed four documents with a fixed clock so results are reproducible. The emulator stores the documents; your application decides which are expired. Do not sleep and claim the emulator is waiting for the managed sweeper.
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, Timestamp } from "firebase-admin/firestore";initializeApp({ projectId: process.env.GCLOUD_PROJECT });const db = getFirestore();const now = Timestamp.fromMillis(Date.UTC(2026, 8, 17, 2, 0, 0));const hour = 60 * 60 * 1000;const docs = [ ["sessions/s-expired", {kind:"session", ownerId:"u-a", retentionClass:"session-30d", expireAt:Timestamp.fromMillis(now.toMillis()-hour), legalHold:false}], ["sessions/s-active", {kind:"session", ownerId:"u-a", retentionClass:"session-30d", expireAt:Timestamp.fromMillis(now.toMillis()+hour), legalHold:false}], ["events/e-expired", {kind:"event", tenantId:"tenant-a", retentionClass:"event-90d", expireAt:Timestamp.fromMillis(now.toMillis()-hour), legalHold:false}], ["audit/a-hold", {kind:"audit", tenantId:"tenant-a", retentionClass:"audit-7y", expireAt:null, legalHold:true}],];for (const [path,data] of docs) await db.doc(path).set(data);console.log(JSON.stringify({seeded:docs.length, clock:now.toDate().toISOString()}));
export function retentionDecision(doc, nowMs) { if (doc.legalHold === true) return { eligible:false, reason:"LEGAL_HOLD" }; if (doc.retentionClass === "audit-7y") return { eligible:false, reason:"POLICY_REQUIRES_ARCHIVE_OR_HOLD_REVIEW" }; const expireMs = doc.expireAt?.toMillis?.() ?? null; if (expireMs === null) return { eligible:false, reason:"NO_EXPIRATION" }; return expireMs <= nowMs ? { eligible:true, reason:"EXPIRED" } : { eligible:false, reason:"NOT_YET_EXPIRED" };}// Product visibility should use the same business clock semantics; it must not wait for physical TTL deletion.
import assert from "node:assert/strict";// Assume seed-retention.mjs has run and nowMs is fixed.const nowMs = Date.UTC(2026,8,17,2,0,0);const expired = retentionDecision({legalHold:false,retentionClass:"session-30d",expireAt:{toMillis:()=>nowMs-1}}, nowMs);const active = retentionDecision({legalHold:false,retentionClass:"session-30d",expireAt:{toMillis:()=>nowMs+1}}, nowMs);const hold = retentionDecision({legalHold:true,retentionClass:"audit-7y",expireAt:null}, nowMs);assert.equal(expired.eligible, true);assert.equal(active.eligible, false);assert.equal(hold.reason, "LEGAL_HOLD");console.log("eligibility-contract: PASS");
Expected state: expired documents still exist in the emulator, but the product-visibility decision rejects them. That proves your policy logic; it does not prove production deletion lag, billing, listener delivery timing, or TTL metrics.
6. Controlled failure: use TTL as a reservation timer
Suppose inventory is reserved until 14:05 and the system expects
TTL to delete the reservation at 14:05:00 so stock is released.
Because TTL is asynchronous, inventory can remain reserved long
after the business deadline. The repair is to make
expiresAt <= now part of the reservation state
machine and run an idempotent expiry/compensation worker for
prompt business action; TTL then removes stale reservation
records later.
TTL proves eventual storage cleanup after eligibility. It does not prove exact scheduling, ordered execution, transactional multi-document changes, or business compensation.
7. Optional managed verification
Use a throwaway billed project only. Enable a TTL policy, write
a tiny bounded set of documents, record
expiredAt and actual disappearance time, and query
Cloud Monitoring for deletion count and expiration-to-deletion
delay. Do not infer a universal p95/p99 from a handful of
documents; record the sample as evidence for that environment
only.
gcloud firestore fields ttls update expireAt \ --collection-group=sessions \ --enable-ttl \ --project=YOUR_ISOLATED_BILLED_PROJECTgcloud firestore fields ttls list --collection-group=sessions \ --project=YOUR_ISOLATED_BILLED_PROJECTgcloud firestore operations list --project=YOUR_ISOLATED_BILLED_PROJECT
Production judgment
Use TTL when deletion can be eventual and policy can be expressed by a field. If the requirement is “stop access at an exact instant,” enforce that in application authorization/query logic. If it is “delete these records together,” use an explicit transactional/batch workflow where feasible. If it is “retain until litigation hold clears,” TTL should be disabled or withheld for held data until governance approves deletion.
Verification checklist and cleanup
- Expiration and physical deletion are documented as distinct states.
- Product reads/actions reject expired state without waiting for TTL.
- Subcollections are included in lifecycle design.
- Delete-trigger side effects are idempotent.
- Real-project TTL verification is isolated, billed, bounded, and removed after evidence capture.
- Local emulator data can be reset safely.
Bridge to Lesson 2
Lesson 2 moves from lifecycle semantics to write-path mechanics: TTL timestamps are often monotonically increasing, and indexing them unnecessarily can create the same sequential-index hotspot problem introduced in Chapter 15.
Knowledge check
-
Does a past
expireAtmake a document disappear immediately? - Can an expired document still appear in a query?
- Does TTL cascade to subcollections?
- What if an expired-but-not-yet-deleted document gets a later TTL value?
- Why is the emulator lab valuable if it does not sweep TTL?
Review the answers
1. No. It becomes eligible; managed deletion is asynchronous and typically occurs within about 24 hours.
2. Yes, until the TTL process actually deletes it. Product logic must not equate existence with validity.
3. No. Parent deletion does not automatically delete nested subcollections.
4. Firestore honors the latest TTL value, so the document can become non-expired before deletion.
5. It proves deterministic application lifecycle/visibility rules without fabricating managed deletion behavior.
Summary and next step
This lesson established the working contract for TTL Policies and Expiration Fields: Asynchronous Deletion Semantics and Non-Instant Guarantees. Keep its edition/mode assumptions, trust boundary, verification evidence, and operational constraints explicit when reusing the pattern.
Next, continue to Index Exemptions for TTL Fields, Sequential Timestamp Costs, and Write Throughput Considerations.
Authoritative references
- Firebase · Manage data retention with TTL policies
- Firebase · Firestore index overview and index exemptions
- Firebase · Cloud Firestore pricing and TTL billing
- Firebase · Firestore quotas and limits
- Firebase · Cloud Firestore triggers
- Firebase · Enterprise TTL indexes
- Google Cloud · MongoDB compatibility TTL indexes
- Google Cloud · MongoDB compatibility release notes
- Google Cloud · Firestore Monitoring metrics