Session cleanup at scale
When you're running a production system with millions of active sessions, manually deleting expired ones becomes a nightmare. You either waste resources polling the database constantly, or you risk accumulating stale records that bloat your collections and slow down queries. MongoDB's TTL (time-to-live) indexes solve this elegantly by automating document expiration. Instead of writing background jobs or cron tasks, you simply index a date field with expireAfterSeconds, and MongoDB handles the rest. In this guide to TTL indexes for expiring data: best practices explained, we'll walk through how to implement them correctly, what pitfalls to avoid, and when they're the right choice for your team.
Index a date field with expireAfterSeconds
The core mechanism of TTL indexes is straightforward: you create a special index on a date field and tell MongoDB how many seconds before a document should expire. MongoDB's background TTL monitor then checks that field periodically and removes documents whose timestamp has passed.
Here's how to create a TTL index on a sessions collection:
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 }
);In this example, any document in the sessions collection will be deleted automatically 3600 seconds (1 hour) after its createdAt timestamp. When you insert a new session, just set the createdAt field to the current date:
db.sessions.insertOne({
userId: "user123",
token: "abc123def456",
createdAt: new Date()
});The document will remain in the collection for exactly one hour, then disappear without any application code triggering the deletion. This is particularly powerful in Node.js applications, where you can use the driver to create the index during initialization:
const sessionsCollection = db.collection('sessions');
await sessionsCollection.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 }
);
// Insert a session
await sessionsCollection.insertOne({
userId: req.user.id,
token: generateToken(),
createdAt: new Date()
});You can also use a different field name if you prefer. For instance, if your documents use expiresAt instead of createdAt, just index that field. The only requirement is that the field contains an ISODate value (or null), and the index must be sparse if you want to allow documents without that field.
Great for sessions and temporary tokens
TTL indexes shine when you have data that's naturally temporary. Sessions, refresh tokens, one-time passwords, password reset links, and rate-limit counters are all ideal candidates. In a typical SaaS platform, user sessions might have a 24-hour lifetime. Rather than running a nightly cleanup job that locks the collection and deletes millions of rows, TTL indexes quietly expire them in the background without blocking reads or writes.
Consider a real-world scenario: you're building an authentication service that issues tokens with a 15-minute expiry. Your tokens collection might look like this:
db.tokens.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 }
);Notice we used expireAfterSeconds: 0. This means the document expires exactly when the timestamp matches. When you create a token, you calculate its expiry in advance:
const now = new Date();
const expiresAt = new Date(now.getTime() + 15 * 60 * 1000); // 15 minutes from now
await tokensCollection.insertOne({
userId: "user456",
token: "xyz789",
expiresAt: expiresAt
});The token document vanishes automatically after 15 minutes, no manual cleanup required. This approach reduces application complexity significantly. You don't need separate services monitoring and purging old tokens—the database handles it transparently.
Another common use case is temporary two-factor authentication codes. These might expire after 5 minutes, and storing hundreds of millions of expired codes in your collection is wasteful. With TTL indexes, they're gone automatically.
Deletion runs every minute
It's crucial to understand that MongoDB's TTL deletion process is not real-time. The background TTL monitor thread runs approximately every 60 seconds to find and delete expired documents. This means if a document expires at 14:32:00, it might not be deleted until 14:33:00 or later—you cannot rely on sub-second precision.
In most applications, this granularity is fine. A session that's supposed to expire after 1 hour being deleted 30 seconds late doesn't break anything. However, there are scenarios where this matters. If you're tracking rate limits and need exact enforcement, the 60-second window could allow slightly more requests than intended. Similarly, if you're issuing short-lived tokens (5-10 minute lifetimes), the deletion delay is negligible compared to the token validity window.
The frequency of the TTL monitor can be affected by your MongoDB configuration, but as a rule, you should assume deletions happen roughly once per minute. This is documented behavior, not a bug. Plan your expiry times accordingly—if you need something gone within seconds, a TTL index isn't the right tool.
On deployments with replication, TTL deletions happen on the primary node and are replicated to secondaries. On sharded clusters, each shard runs its own TTL monitor independently, so there's no coordination overhead.
Not precise to the second
Building on the previous point: do not rely on TTL indexes for precise expiry enforcement. The combination of the 60-second check interval and potential clock skew across your infrastructure means a document could theoretically exist for up to a minute longer than expected.
If you need strict enforcement (for example, in a security-critical context), validate expiry on the application side. When a user submits a token or password reset code, check the expiresAt timestamp in your application before accepting it. Don't assume it's gone from the database:
async function validateToken(tokenString) {
const tokenDoc = await tokensCollection.findOne({ token: tokenString });
if (!tokenDoc) {
throw new Error('Token not found');
}
// Always check expiry in application code
if (new Date() > tokenDoc.expiresAt) {
throw new Error('Token expired');
}
return tokenDoc;
}This dual approach gives you both the automatic cleanup benefit of TTL indexes and the precise enforcement your security posture demands. The TTL index prevents old tokens from bloating your collection; your application logic ensures they can't be used.
Common mistakes
Several pitfalls commonly trip up teams implementing TTL indexes for the first time.
Mistake 1: Creating a TTL index on a non-date field. If you index a string, number, or other non-date field with expireAfterSeconds, MongoDB will ignore the TTL behavior. Always use ISODate fields. If you're storing timestamps as Unix epoch numbers, convert them to proper Date objects first.
Mistake 2: Forgetting to handle null or missing values. If a document doesn't have the TTL field, it won't expire. This can silently accumulate documents you thought were temporary. Use sparse indexes if you want to exclude documents without the field, or ensure every document has the field populated:
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600, sparse: true }
);With sparse: true, documents without a createdAt field are ignored by the TTL monitor and persist indefinitely. Without it, the behavior is the same—the field must exist and be a date for expiry to work.
Mistake 3: Assuming deletion is immediate. We've covered this, but it bears repeating: don't write code that depends on a document being gone within seconds. The 60-second cycle means there's a grace period.
Mistake 4: Creating multiple TTL indexes on the same collection. MongoDB only allows one TTL index per collection. If you try to create a second one, it will either fail or overwrite the first, depending on your MongoDB version. Design your expiry strategy upfront.
Mistake 5: Changing expireAfterSeconds on an existing index. You can't directly modify a TTL index. If you need different expiry logic, drop the old index and create a new one. This can cause downtime in high-traffic systems, so plan migrations carefully.
Key takeaways
- Create a TTL index by specifying a date field and
expireAfterSecondsvalue; MongoDB automatically deletes documents when the timestamp exceeds the threshold. - TTL indexes are perfect for sessions, tokens, OTPs, and other naturally temporary data—they eliminate the need for background cleanup jobs.
- Deletion runs approximately once per minute in the background; don't expect sub-second precision.
- Always validate expiry in your application code for security-critical scenarios; use TTL indexes for bulk cleanup, not as your sole expiry enforcement mechanism.
- Ensure the TTL field is always populated as a proper ISODate, and be aware that documents without the field will not expire.
- Remember that you can only have one TTL index per collection, and modifying it requires dropping and recreating.






Comments
Loading comments…