$changeStream is a MongoDB aggregation stage that opens a live cursor of change events. Instead of polling with find(), MongoDB pushes each insert, update, or delete to your app.
Remember
$changeStream = "notify me when data changes"
// Easy helper (preferred in apps)
db.orders.watch()
// Explicit stage (must be first)
db.orders.aggregate([
{ $changeStream: {} }
])
needs replica set or sharded cluster
event._id → resume token
fullDocument: "updateLookup" → whole doc on updates
Polling find() → use change streams instead
Check deployment.rs.status() confirms replica set mode—required for change streams.
Seed one document. A starting order gives you something to update in later demos.
Two sessions help. Keep a stream open in Session A; write in Session B.
Example 2 — Basic Watch
Goal: open a change stream, insert a new order, and print the insert event.
mongosh
// Session A — open the stream
const stream = db.orders.watch()
// Session B — make a change
db.orders.insertOne({
_id: 2,
product: "Mouse",
status: "pending",
amount: 25
})
// Session A — read one event
const event = stream.next()
printjson(event)
// Same idea with the explicit stage:
// const stream = db.orders.aggregate([
// { $changeStream: {} }
// ])
Open the cursor.watch() starts listening; it blocks until a change arrives.
Write in another session. The insert becomes a committed oplog entry.
Read the event.stream.next() returns one change document with operationType and fullDocument.
Save _id. That resume token lets you reconnect later with resumeAfter.
Example 3 — Filter Inserts
Goal: keep only insert events. Updates are ignored by the pipeline. Also show how updateLookup loads the full document on updates when you do want them.
mongosh
// Inserts only — pipeline filters after the stream opens
const inserts = db.orders.watch([
{ $match: { operationType: "insert" } }
])
db.orders.updateOne(
{ _id: 1 },
{ $set: { status: "shipped" } }
)
// → no event on this inserts-only stream
db.orders.insertOne({
_id: 3,
product: "Monitor",
status: "pending",
amount: 180
})
// → inserts.next() returns the insert event
// When you need the full document after updates:
const withLookup = db.orders.watch([], {
fullDocument: "updateLookup"
})
// Explicit stage form:
// db.orders.aggregate([
// { $changeStream: { fullDocument: "updateLookup" } }
// ])
Output (simplified)
updateOne → ignored by $match
insertOne → operationType: "insert"
fullDocument.product: "Monitor"
// with updateLookup on a later update:
operationType: "update"
fullDocument.status: "shipped"
updateDescription.updatedFields: { status: "shipped" }
How It Works
Pipeline filters events. Stages after the stream act on each change document.
$match on operationType. Updates never reach your app on the inserts-only cursor.
Options vs stage. Pass fullDocument as the second argument to watch(), or inside { $changeStream: { … } }.
Use case. Inserts-only feeds power ETL; updateLookup powers cache/search sync.
Edge Cases & Pitfalls
Check these before shipping a change-stream worker.
deploy
Standalone mongod will fail
Without a replica set, opening a stream errors. Convert local MongoDB to a single-member replica set for practice.
resume
Save the resume token
Persist event._id before acknowledging work. On restart, pass resumeAfter. After invalidate, use startAfter.
project
Do not drop _id casually
If a later stage removes the resume token, automatic resume becomes harder. Keep _id when you need reconnect safety.
idempotent
Events may be redelivered
After reconnect, design handlers so processing the same change twice is safe (idempotent writes).
Analysis
Performance Notes
Approach
Cost
Best for
watch() / $changeStream
Push; waits on oplog
Live UI, sync, triggers
Stream + $match
Fewer events over the wire
Inserts-only or filtered feeds
Polling find()
Repeated queries
Tiny scripts only
Filter early with $match so your app only receives useful events. Prefer change streams over tight polling loops on busy collections.
Remember
Key Takeaways
Role: push live change events instead of polling.
Helper: prefer watch(); use $changeStream as the explicit first stage.
Resume: save event._id; reconnect with resumeAfter.
Requirement: replica set or sharded cluster.
One line:$changeStream answers “tell me every time this collection changes.”
Frequently Asked Questions
$changeStream opens a live cursor of change events on a collection (or database/cluster when configured). Each event describes an insert, update, replace, delete, or related change—useful for dashboards, sync, and cache invalidation.
As the first stage of aggregate(), or use db.collection.watch(), which opens the stream for you. Later stages such as $match can filter events.
No. Change streams need a replica set or sharded cluster (they read the oplog). For local practice, start a single-node replica set.
watch() is the recommended helper: pass filter stages in the pipeline and options like fullDocument or resumeAfter as the second argument. aggregate([{ $changeStream: { … } }]) is the same idea with the stage written explicitly.
For update events, MongoDB includes the current majority-committed document in fullDocument. Without it, updates often show only updateDescription (changed fields).
Save the event _id (resume token). On reconnect, pass resumeAfter: token. Use startAfter after an invalidate event (for example, collection drop). resumeAfter, startAfter, and startAtOperationTime are mutually exclusive.