Treat TTL as asynchronous background retention: measure eligibility, deletion lag, metrics, Date/array semantics, collMod changes, and operational deletion cost.
TTL Indexes: Expiration Semantics, Background Deletion, and Time-Based Retention
Batch heterogeneous writes safely, interpret partial success, compare ordered and unordered execution, and use modern cross-namespace bulk APIs without assuming all-or-nothing behavior.
Learning objectives
Distinguish logical expiration eligibility from the later background delete operation.
Use absolute-date TTL with expireAfterSeconds:0 and reason about missing, wrong-type, and date-array fields.
Observe TTL progress through collection state and serverStatus metrics instead of assuming a precise deletion instant.
Change a TTL interval with collMod while understanding mass-deletion and fragmentation risk.
Carry TTL semantics into replica-set, retention, security, and capacity decisions.
This lesson pins MongoDB Community Server
8.3.8 with
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
and mongosh 2.10.0. The server is a disposable
standalone published only on loopback
127.0.0.1:27068. Authentication and TLS are
disabled only for this isolated lab. Feature Compatibility
Version (FCV) is observed but never changed. Default read/write
concern and primary read preference apply. Atlas, KMS, and
Enterprise Advanced are not mandatory. Runtime output shown as
“expected” is documentation-derived because this generation
environment has no Docker/mongod/mongosh runtime.
TTL is a deletion mechanism whose timing depends on a background
process. The lab therefore records timestamps, remaining
documents, and metrics.ttl; it never treats
“expired at T” as proof that the document is physically absent
at T.
1. Expiration time is not deletion time
A time-to-live (TTL) index is a special single-field index on a Date field (or an array containing Dates). A document becomes eligible after the configured threshold, but MongoDB removes eligible documents through a background TTL process. The TTL index does not guarantee that expired data is deleted immediately; the current documentation states that the monitor runs periodically and deletion can lag under workload. Therefore TTL is suitable for retention, sessions, caches, and cleanup—not for a security rule that requires a record to become inaccessible at an exact millisecond.
On replica sets, the TTL background thread deletes only on the primary. Secondaries replicate those delete operations. This standalone lab demonstrates eligibility and deletion mechanics but cannot reproduce election/failover behavior.
2. Seed past, future, missing, wrong-type, and array dates
docker rm -f atlasmart-mongo-ch11-l2 2>/dev/null || truedocker volume rm atlasmart-mongo-ch11-l2-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch11-l2 \ -p 127.0.0.1:27068:27017 \ -v atlasmart-mongo-ch11-l2-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27068/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(),hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))'
const c=db.sessions_ch11_l2;c.drop();const now=new Date();c.insertMany([ {_id:1,sessionId:"expired-past",createdAt:now,expiresAt:new Date(now.getTime()-120000)}, {_id:2,sessionId:"near-future",createdAt:now,expiresAt:new Date(now.getTime()+30000)}, {_id:3,sessionId:"far-future",createdAt:now,expiresAt:new Date(now.getTime()+3600000)}, {_id:4,sessionId:"missing-expiry",createdAt:now}, {_id:5,sessionId:"wrong-type",createdAt:now,expiresAt:"2026-09-02T12:00:00Z"}, {_id:6,sessionId:"array-earliest-wins",createdAt:now,expiresAt:[new Date(now.getTime()+3600000),new Date(now.getTime()-60000)]}]);printjson({now,docs:c.find().sort({_id:1}).toArray()});
const c=db.sessions_ch11_l2;print(c.createIndex({expiresAt:1},{name:"ttl_expiresAt",expireAfterSeconds:0}));printjson(c.getIndexes());printjson(db.serverStatus().metrics.ttl);
Expected semantics, not an exact wall-clock schedule:- expireAfterSeconds:0 means the Date value itself is the expiration time.- expired-past and array-earliest-wins are immediately eligible, but deletion is asynchronous.- array-earliest-wins uses the earliest Date in the indexed array for the threshold.- missing-expiry never expires through this TTL index.- wrong-type is not a Date, so it does not expire through this TTL index.- near-future becomes eligible after its Date passes, but physical deletion can occur later.- TTL deletions are observable in serverStatus().metrics.ttl and are performed by the primary in a replica set.- A compound TTL design is invalid: TTL is a single-field index property.
3. Poll instead of pretending TTL is immediate
The bounded polling loop waits at most about 130 seconds. It is intentionally tolerant: the expected invariant is eventual removal of eligible Date-valued documents, not a fixed number of seconds after eligibility.
const c=db.sessions_ch11_l2;const start=new Date();for(let attempt=0;attempt<26;attempt++){ const rows=c.find({},{_id:0,sessionId:1,expiresAt:1}).sort({sessionId:1}).toArray(); const ttl=db.serverStatus().metrics.ttl; printjson({attempt,at:new Date(),remaining:rows,ttl}); if(!c.findOne({sessionId:"expired-past"}) && !c.findOne({sessionId:"array-earliest-wins"})) break; sleep(5000);}printjson({elapsedMs:new Date()-start,remaining:c.find({},{_id:0,sessionId:1,expiresAt:1}).sort({sessionId:1}).toArray()});
If a token, entitlement, or legal hold must stop being usable at an exact business deadline, enforce that deadline in the read/authorization predicate as well. TTL can clean up storage later.
4. Changing retention is an operational event
collMod can convert an existing single-field index
to TTL or change expireAfterSeconds. Reducing the
interval may make a large historical population immediately
eligible, creating delete load and storage fragmentation. In
production, estimate the eligible population first and consider
batched manual cleanup before shortening retention.
const c=db.sessions_ch11_l2;printjson(db.runCommand({ collMod:"sessions_ch11_l2", index:{name:"ttl_expiresAt",expireAfterSeconds:120}}));printjson(c.getIndexes());
5. Deliberately wrong: compound TTL assumption
TTL is not an option you can attach meaningfully to an arbitrary compound index. The retention index itself must be single-field; design other query indexes separately.
const c=db.sessions_ch11_l2;try{ print(c.createIndex({createdAt:1,expiresAt:1},{name:"wrong_compound_ttl",expireAfterSeconds:60}));}catch(e){ printjson({name:e.name,code:e.code,message:e.message}); }printjson(c.getIndexes());
6. Verification, cleanup, and production judgment
Verification checklist
-
The TTL index metadata shows
expireAfterSeconds. - Past Date values are eligible before future Date values.
- An array uses its earliest Date to calculate eligibility.
- Missing and non-Date fields are not treated as ordinary TTL dates.
- The polling output distinguishes eligibility from actual deletion time.
-
metrics.ttlis captured before/after observation. -
The interval is modified with
collMod, not by recreating an equivalent index.
Production judgment. TTL reduces application cleanup code but creates write/delete I/O and potentially bursty reclaim work. Monitor delete metrics, replication lag, cache/disk pressure, and the population near the retention boundary. Schema validation can ensure the TTL field is a Date. Test retention changes on production-like volumes, define rollback before shortening intervals, and never confuse replicated TTL deletion with backup or archival. Lesson 3 moves from time-based membership to schema-based flexibility: wildcard indexes can index unknown paths, but that convenience also expands maintenance scope.
docker rm -f atlasmart-mongo-ch11-l2docker volume rm atlasmart-mongo-ch11-l2-data
Check your understanding
- Does a document disappear exactly when its TTL expires?
- What does expireAfterSeconds:0 mean for a Date field?
- What happens when the TTL field is an array of Dates?
- Which replica-set member performs TTL background deletes?
- Why can shortening a TTL interval be risky?
Review the answers
1. No. Expiration makes it eligible; a background process deletes it later, and lag can exceed the nominal monitor interval under load.
2. The Date value itself is the absolute expiration time.
3. MongoDB uses the earliest Date value to compute the expiration threshold.
4. The primary; secondaries replicate the resulting deletes.
5. A large historical population can become eligible at once, creating delete load, replication work, and storage fragmentation.
Authoritative references
- MongoDB 8.3 release notes — Current 8.3 baseline and patch-sensitive behavior; re-check before reproduction.
- Index types — Current index-family overview and boundaries.
- Explain results — Planner and execution evidence used throughout this chapter.
- mongosh changelog — mongosh version used for chapter commands.
- TTL indexes — Expiration, background deletion, metrics, replica behavior, restrictions, and collMod changes.
- Schema validation — Useful for enforcing a consistent Date-valued TTL field.