Use Change Streams
Learn to use change streams in MongoDB to react to data changes in real time. This tutorial covers the core concept, a hands-on exercise, troubleshooting, and what to study next.
Focus: use change streams to react to data changes
Your application is built, deployed, and users are interacting with it. Then it happens again — a record changes, a new order lands, a user updates their profile, and you have no idea. Polling feels clunky, webhooks require external infrastructure, and your database remains a silent black box. MongoDB's change streams solve this by giving you a real-time, ordered, and resumable feed of data changes directly from the database. In this lesson, you'll learn how to use change streams to react to data changes, turning your MongoDB deployment from a passive store into an active event source.
1. The problem this lesson solves
Traditional ways to detect data changes are reactive by nature and full of trade-offs. Polling, for example, queries the database every few seconds looking for new or updated documents. It's simple but wasteful — you're constantly hammering the database for data that may not exist. It also introduces latency: a change that happens one millisecond after a poll isn't seen until the next poll, and the database load scales with every client that polls.
Webhooks and message queues (like RabbitMQ or Apache Kafka) solve the latency problem, but they add external infrastructure. You now have to operate a separate system, handle network failures, and write code to bridge your database and that system. Worse, they depend on your application to propagate changes — if your app crashes mid-write, the notification is lost.
Change streams flip the model. Instead of your application asking "what changed?", the database tells you what changed. MongoDB watches collections, databases, or entire deployments and emits a change event — a document that describes the operation (insert, update, delete, replace) and the affected data — in near real time. Your code stays simple, your infrastructure stays lean, and your reaction time drops to milliseconds.
This lesson is part of the MongoDB track, and it builds directly on your understanding of collections, queries, and the aggregation pipeline. By the end, you'll be able to wire your MongoDB cluster into your application's event-driven architecture.
2. Core concept / mental model
Think of a change stream as a live subscription to your data. It's like a river: documents flow downstream, and you stand on the bank with a net that catches every change that floats by. You don't need to pull the river toward you (polling); you just watch, and the river brings you what you need.
Behind the scenes, change streams use the MongoDB oplog (operations log), a capped collection that records every write operation. When you open a change stream, MongoDB reads the oplog and streams changes to your client in the order they occurred. Because the oplog is ordered and durable, you get a guarantee that change events are delivered in the same order as the writes — critical for features like inventory management or financial transactions.
Key terms you'll encounter:
- Change event: A document that describes a single database change. It includes
operationType,ns(namespace: database and collection),documentKey, andfullDocument(for inserts and replaces). - Resume token: A unique identifier for a point in the change stream. You can save it and resume from that point later, even after a client crash.
- Pipeline: Change streams accept an aggregation pipeline, allowing you to filter and transform events before they reach your application.
Change streams are available in replica sets and sharded clusters (including MongoDB Atlas), but not in standalone instances. This is because the oplog only exists on replica sets. If you're on a standalone server, you'll need to convert it to a single-node replica set — we'll cover that in the hands-on section.
3. How it works step by step
Step 1: Ensure your deployment is change-stream-ready
Change streams require a replica set or a sharded cluster. MongoDB Atlas clusters (M0 and above) are replica sets by default, so they work out of the box. For a local MongoDB instance, you'll need to initialize it as a single-node replica set.
Step 2: Open a change stream
From your application, you open a stream on a collection, a database, or the entire cluster. The most common approach is collection-level:
import pymongo
client = pymongo.MongoClient("mongodb://localhost:27017/")
db = client["shop"]
orders = db["orders"]
# Open a change stream on the 'orders' collection
with orders.watch() as stream:
for change in stream:
print(change)
# React to the change
process_order(change)
This blocks and yields change events as they occur. The watch() method returns a CommandCursor that you can iterate over.
Step 3: Filter and transform with an aggregation pipeline
You can pass a pipeline to watch() to only receive events you care about. For example, you might want to ignore all inserts and only watch updates to the status field:
pipeline = [
{"$match": {"operationType": "update", "updateDescription.updatedFields.status": {"$exists": True}}}
]
with orders.watch(pipeline=pipeline) as stream:
for change in stream:
print(change)
Step 4: Handle resume tokens
Change streams are resumable. If your application crashes, you can restart the stream from the last processed token, avoiding duplicate processing or missed events. The resume_token field in each change event contains the necessary data.
resume_token = None
with orders.watch() as stream:
for change in stream:
process(change)
resume_token = change["_id"] # save this
# Later, resume from that token
with orders.watch(resume_after=resume_token) as stream:
for change in stream:
process(change)
Step 5: React in your application
The change event is a JSON document. You can trigger any action: update a cache, send a notification, synchronize a search index, or feed an analytics dashboard. The key is that you decide what "react" means for your use case.
4. Hands-on walkthrough
Now let's put this into practice. We'll set up a local environment, enable change streams, and build a small Python script that reacts to new orders.
Create a replica set (if needed)
If you have a standalone MongoDB, convert it to a single-node replica set by starting mongod with the --replSet flag and initializing it:
# Start mongod as a single-node replica set
mongod --dbpath /data/db --replSet rs0
# In another terminal, connect and initialize
mongosh --eval "rs.initiate()"
You should see output like { "ok": 1 }.
Build a change-stream listener
Create a file watch_orders.py:
import pymongo
import json
client = pymongo.MongoClient("mongodb://localhost:27017/")
db = client["shop"]
orders = db["orders"]
# Open a change stream that only listens for inserts
pipeline = [{"$match": {"operationType": "insert"}}]
print("Listening for new orders...")
with orders.watch(pipeline=pipeline) as stream:
for change in stream:
order = change["fullDocument"]
print(f"New order received: {json.dumps(order, indent=2)}")
# Example reaction: log and send to a shipping service
print(f"Shipping order {order['_id']} to {order['customer']}")
Now, open a separate terminal and insert a document into the orders collection:
mongosh --eval 'db.orders.insertOne({customer: "Alice", total: 129.99, status: "pending"})' shop
In your listener terminal, you'll see output similar to:
Listening for new orders...
New order received: {
"_id": "...",
"customer": "Alice",
"total": 129.99,
"status": "pending"
}
Shipping order ObjectId('...') to Alice
The change event arrived nearly instantly, with no polling.
React to updates with a resume token
Let's build a more robust example that resumes after a crash. We'll save the resume token to a file after each event:
import pymongo
import json
import os
client = pymongo.MongoClient("mongodb://localhost:27017/")
orders = client["shop"]["orders"]
TOKEN_FILE = "resume_token.json"
def load_token():
if os.path.exists(TOKEN_FILE):
with open(TOKEN_FILE) as f:
return json.load(f)
return None
def save_token(token):
with open(TOKEN_FILE, "w") as f:
json.dump(token, f)
resume_token = load_token()
opts = {"resume_after": resume_token} if resume_token else {}
with orders.watch(**opts) as stream:
for change in stream:
print("Change:", change)
# Process the change...
save_token(change["_id"])
Now if your script crashes, it will resume from the last processed event on the next run, avoiding duplicates.
5. Compare options / when to choose what
Change streams aren't the only way to react to data changes. Here's a comparison with other common approaches:
| Approach | Latency | Infrastructure overhead | Durability | Best for |
|---|---|---|---|---|
| Polling | High (seconds) | None (DB only) | Depends on app logic | Low-volume, non-critical checks |
| Webhooks / Message queues | Low | High (separate system) | Strong (if designed well) | Cross-system integrations |
| Change streams | Very low (ms) | Low (built into MongoDB) | Strong (resumable tokens) | Real-time reactions within your app |
When to choose change streams: - You need sub-second reaction time. - You want to avoid extra infrastructure (no Kafka or RabbitMQ). - You're building a microservice or a real-time feature (e.g., live dashboard, cache invalidation, search index sync).
When to consider alternatives: - If you're already running Kafka for event streaming and need multi-source aggregation, a message queue may be a better fit. - If you need to notify external systems over HTTP, webhooks are more direct (though you can bridge change streams to webhooks yourself).
Variations
- Full document lookup: For update operations, change events don't include the entire document by default. You can set
full_document="updateLookup"to fetch the latest version in the event. - Database/cluster-wide streams: Instead of collection-level, use
client.watch()for all collections in the cluster, ordb.watch()for all collections in a database. - Pre/post-images: In MongoDB 6.0+, you can configure streams to include the document before and/or after an update, which is handy for auditing.
6. Troubleshooting & edge cases
"Change streams are not supported on standalone instances"
Error: pymongo.errors.OperationFailure: The $changeStream stage is only supported on replica sets
Fix: Convert your instance to a replica set as shown in the hands-on section. In Atlas, ensure you're not on a free M0 cluster? Actually, M0 supports change streams, but you must wait a few seconds after the cluster is created.
No events after inserting a document
- Cause: Your change stream pipeline filters out the operation type.
- Fix: Check your
$matchcondition. If you're filtering foroperationType: "insert", yourwatch()pipeline must include that condition at the start. Also, verify that the change event'soperationTypeis exactly what you expect (e.g.,'update'for updates). - Hidden gotcha: In Python, the
$matchstage must be the first stage in the pipeline. If you put$projectfirst, it will fail.
Duplicate events after a crash
- Cause: Your resume token wasn't saved, or you resumed from a token that's older than the last processed event.
- Fix: Save the
_idof each change after processing it, not before. If you process and then crash, you may miss an event; if you save before processing, you may process twice on duplicate events. Use the resume token to restart exactly from the last saved event.
Full document is missing for updates
- Cause: By default, change events for updates include only the fields that changed.
- Fix: Pass
full_document="updateLookup"to thewatch()method to include the full document. Note that for deletes, nofullDocumentis included (you have to store the document key).
Stream stops silently in production
- Change streams time out after 10 seconds of inactivity (the
maxAwaitTimeMSdefault). Your application should handleServerSelectionTimeoutErrorand resume. In Python, thewith orders.watch(...)block will raise an exception if the connection is lost; you need to catch it and start a new stream with the last resume token.
Pro tip: Always use a resume token for any production change stream. It's the difference between a robust event-driven system and a flaky one.
7. What you learned & what's next
You now understand how to use change streams to react to data changes. Let's recap the key points:
- Core idea: Change streams provide a real-time, ordered, resumable feed of database changes via the oplog.
- Practical application: You can open a stream, filter with an aggregation pipeline, and react to events in your application with milliseconds of latency.
- Resume tokens: They ensure you never miss an event, even after a crash.
- Comparison: Change streams are often simpler and more lightweight than polling or external message queues.
- Troubleshooting: You know common errors like standalone instances, pipeline order, and full document vs. update operations.
You've covered learning objective 1 (explain the core idea) and objective 2 (complete a practical exercise). You're now ready to integrate change streams into your own projects.
Next in the MongoDB track, you'll learn how to build a real-time application with MongoDB — combining change streams with a Node.js or Python web server, and you'll see how to scale your event-driven architecture as your data grows. Keep your resume token handy — you'll reuse that pattern in the next lesson.
Practice recap
Now try building a small real-time log monitor: set up a change stream on any collection, then insert a document and watch it print. Next, modify the pipeline to only match updates to a specific field, and use a resume token to restart your stream after a simulated crash. This hands-on will cement your understanding of the core concepts and prepare you for the next lesson on real-time applications.
Common mistakes
- Trying to use change streams on a standalone MongoDB instance without converting it to a replica set — you'll get an OperationFailure.
- Placing a $project or $group stage before $match in the change stream pipeline — it breaks the stream; $match must be the first stage.
- Assuming update change events include the full document by default — they only include changed fields unless you set full_document='updateLookup'.
- Not saving a resume token, then losing your stream on a crash and having to reprocess from the beginning or missing events entirely.
Variations
- Use full_document='updateLookup' to include the latest document version in update events, useful for caching or audit logs.
- Open a database-wide or cluster-wide change stream (db.watch() or client.watch()) instead of collection-level to monitor all collections.
- Pre/post-images (MongoDB 6.0+) let you capture the document state before and after an update, which is great for triggering workflows based on old vs. new values.
Real-world use cases
- Automatically invalidate or update a Redis cache when a product's price changes, so your API always serves fresh data without polling.
- Stream new user signups to an analytics pipeline in near real time, feeding a live dashboard or triggering a welcome email.
- Synchronize a search index (e.g., Elasticsearch or Atlas Search) with your MongoDB collection instantly when documents are inserted or updated.
Key takeaways
- Change streams give you a real-time, ordered, resumable feed of insert, update, delete, and replace operations from MongoDB.
- They require a replica set or sharded cluster; standalone instances are not supported.
- You can filter and transform change events using the same aggregation pipeline syntax you already know.
- Resume tokens are crucial for reliability — save the _id of each event after processing so you can restart without missing or duplicating events.
- For updates, the full document is not included by default; use full_document='updateLookup' or pre/post-images when you need the complete state.
- Change streams are often simpler and more efficient than polling or managing external message queues for in-app event reactions.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.