A TTL index deletes documents automatically once a date field passes an age you choose. It is the clean way to expire sessions, logs, one-time tokens, and cache entries without writing a cron job that runs deleteMany at 3am and occasionally forgets to. Set it once and MongoDB does the sweeping.
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
That index deletes each session about a minute after its createdAt turns an hour old. Here is how it works, and the two ways it quietly fails.
The mechanism
A TTL index is an ordinary single-field index with one extra option, expireAfterSeconds. A background thread wakes up roughly every 60 seconds, finds documents whose indexed date is older than the limit, and removes them. So expiry is approximate: a document lives up to a minute past its deadline, because that is how often the sweeper runs. For sessions and logs, close enough. For anything needing to-the-second expiry, TTL is the wrong tool. The TTL index docs spell out the timing.
There is a second style. If you set expireAfterSeconds: 0 and store a specific future date in the field, the document expires at exactly that date rather than an age past it. Useful when each document knows its own expiry, like a coupon with a fixed end date:
db.coupons.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
// a document with expiresAt in the past is swept on the next pass
The rule that trips everyone: it must be a Date
TTL only acts on a BSON Date. This is the single most common reason a TTL index does nothing. If createdAt was written as a string ("2026-06-26T10:00:00Z") instead of an actual Date, the sweeper skips it, and your "self-cleaning" collection grows forever with a completely healthy-looking index sitting on top of it.
// works: real Date
db.sessions.insertOne({ createdAt: new Date() })
// ignored by TTL: string
db.sessions.insertOne({ createdAt: "2026-06-26T10:00:00Z" })
If a TTL collection is not shrinking, check the field type before anything else. That is a schema question, and finding this kind of type mismatch is exactly what analyzing your schema is for.
Changing the TTL without a rebuild
You do not have to drop and recreate the index to change how long documents live. collMod updates it in place:
db.runCommand({
collMod: "sessions",
index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 7200 }
})
Now sessions live two hours instead of one. Existing documents pick up the new deadline on the next sweep.
TTL is storage management too
An expiring collection is one that does not slowly become most of your disk. That makes TTL a quiet ally of storage optimization: the documents leave on schedule, so the collection stops growing without supervision. Just remember that deletes free space for reuse inside the collection but do not shrink the file on disk on their own; that part is compact. And like any index, a TTL index still costs writes, so it belongs in your index budget alongside the rest. For the general index syntax, see how to create an index.
mongoui's Schema doctor catches the string-instead-of-Date case that stops a TTL index cold, and its storage view shows whether the collection is actually shrinking the way the index promises. It runs on your machine, so none of those documents leave your network. Set the TTL, store a real Date, and let old data show itself out.
