MongoDB from Basics to Production · MongoDB in Production

TTL Indexes, Change Streams and Write Concern

Expire documents automatically with TTL indexes, react to changes in real time with change streams, and choose durability with write and read concerns.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Three production features: sessions that expire on their own with a TTL index, a change stream that receives every new order as it's inserted (for cache invalidation or notifications), and writes that wait for the majority of replica set members.

The idea

  • A TTL index (expireAfterSeconds) deletes documents after a date field plus the TTL; a background task runs about every 60 seconds.
  • Change streams (watch()) deliver inserts, updates and deletes in order, resumable with a resume token. They need a replica set.
  • Write concern w: "majority" acknowledges a write only when most members have it, so it survives a failover; read concern "majority" reads only such data.

The code

TTL index, a change stream and majority writes
import os
import threading
import time
from datetime import datetime, timezone
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern

client = MongoClient(os.environ["MONGO_URL"], serverSelectionTimeoutMS=3000)
db = client["ops_example"]

db.sessions.create_index("lastSeen", expireAfterSeconds=1800)
info = next(i for i in db.sessions.list_indexes() if i["name"] == "lastSeen_1")
print("TTL index:", dict(info["key"]), "expireAfterSeconds =", info["expireAfterSeconds"])

orders = db.get_collection("orders", write_concern=WriteConcern(w="majority", wtimeout=5000))
seen = []
with db.orders.watch([{"$match": {"operationType": "insert"}}]) as stream:
    def place_orders():
        time.sleep(0.5)
        for i in (1, 2):
            orders.insert_one({"_id": i, "total": 10 * i, "at": datetime(2026, 1, 15, tzinfo=timezone.utc)})
    threading.Thread(target=place_orders).start()
    for change in stream:
        seen.append(change["fullDocument"]["_id"])
        print("change stream:", change["operationType"], change["fullDocument"]["_id"], change["fullDocument"]["total"])
        if len(seen) == 2:
            token = stream.resume_token
            break
print("resume token saved:", token is not None)
client.drop_database("ops_example")

Output

TTL index: {'lastSeen': 1} expireAfterSeconds = 1800
change stream: insert 1 10
change stream: insert 2 20
resume token saved: True

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Expecting TTL deletion at an exact second; it runs periodically, so filter expired documents in queries if precision matters.
  • Polling a collection for changes instead of using change streams.
  • Not storing the resume token, so a restarted consumer misses changes.
  • w: 1 for critical data, risking loss of acknowledged writes on failover.

Key points

  • TTL indexes expire documents automatically.
  • Change streams deliver ordered, resumable change events.
  • Use majority write concern for data that must survive failovers.

Further reading: TTL indexes · Change streams · Write concern

Practice questions

How precise is TTL deletion?

Answer

Not exact: a background task removes expired documents about every 60 seconds, so documents may live slightly longer.

Why store a change stream's resume token?

Answer

So a restarted consumer can continue from where it stopped without missing or reprocessing events.

What does write concern w: 'majority' guarantee?

Answer

The write is acknowledged only after most replica set members have it, so it won't be rolled back after a failover.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?