mongoui
Indexes·June 26, 2026·6 min read

How to use a TTL index in MongoDB to expire data automatically

A TTL index deletes documents once a date field ages past a limit you set, no cron job required. Here is how to create one, the two rules that decide whether it fires, and why yours might be quietly doing nothing.

Arctic Glass illustration for How to use a TTL index in MongoDB to expire data automatically

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.

Frequently asked questions

How do I create a TTL index in MongoDB?

Create a single-field index on a date field with expireAfterSeconds: db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 }). MongoDB then deletes each document about a minute after its createdAt passes one hour old.

Why is my MongoDB TTL index not deleting documents?

Usually the indexed field is not a BSON Date. TTL only acts on Date values (or arrays of dates); a date stored as a string is ignored and the document never expires. The other common cause is a wrong field: the index is on a field the documents do not actually carry.

How do I change the expireAfterSeconds on an existing TTL index?

Use collMod with the index key: db.runCommand({ collMod: "sessions", index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 7200 } }). You do not have to drop and rebuild the index to change its TTL.

Can a TTL index be a compound index?

No. A TTL index must be on a single field. If you need to expire on a combination of conditions, a single date field plus a partial filter expression, or an application-level cleanup, is the way. The TTL mechanism itself only reads one date field.