The $changeStream stage turns an aggregation pipeline into a live feed of database changes—inserts, updates, deletes, and more—so apps can react instantly instead of polling.
01
Real-time events
Watch collection changes.
02
First stage only
Pipeline must start here.
03
operationType
insert, update, delete…
04
fullDocument
Whole doc on updates.
05
Resume tokens
Survive reconnects.
06
watch() helper
Preferred in mongosh.
Fundamentals
Definition and Usage
$changeStream is an aggregation stage that returns a tailable change stream cursor instead of a one-shot result set. Each event describes what changed, when, and on which namespace (ns.db, ns.coll).
💡
Beginner tip
Change streams need a replica set or sharded cluster—they do not work on a default standalone mongod. For local dev, convert to a single-member replica set. The cursor blocks until a change occurs (or you close it).
Use $changeStream for live notifications, cache invalidation, ETL triggers, and sync to search indexes. In mongosh, db.collection.watch() is usually easier—it builds a pipeline starting with $changeStream automatically.
Foundation
📝 Syntax
$changeStream accepts an options object (all fields optional):
watch() wraps an aggregation whose first stage is $changeStream. The cursor waits until MongoDB commits a change, then returns one event document.
📈 Practical Patterns
Filter events, load full documents, and resume streams.
Example 3 — Watch inserts only with $match
mongosh
const stream = db.orders.watch([
{ $changeStream: {} },
{
$match: {
operationType: { $in: ["insert"] }
}
}
])
// Updates and deletes are filtered out server-side
db.orders.updateOne(
{ product: "Mouse" },
{ $set: { status: "shipped" } }
)
// → no event on this stream
db.orders.insertOne({ product: "Monitor", amount: 180 })
// → stream.next() returns the insert event
📤 Filtered stream:
updateOne → ignored by $match
insertOne → operationType: "insert"
How It Works
Stages after $changeStream process each event like a normal pipeline. $match on operationType is the most common filter for ETL jobs that only care about new rows.
Example 4 — Full document on updates (updateLookup)
By default, updates may only include updateDescription (changed fields). updateLookup performs a lookup so consumers get the full current document—handy for search sync and cache refresh.
Example 5 — Save resume token and reconnect
mongosh
// First connection
let stream = db.orders.watch([{ $changeStream: {} }])
let lastToken = null
while (stream.hasNext()) {
const event = stream.next()
lastToken = event._id // save resume token
processEvent(event) // your app logic
if (shouldStop) break
}
// Later — reconnect without missing events
stream = db.orders.watch([
{
$changeStream: {
resumeAfter: lastToken
}
}
])
// Alternative: aggregate() with $changeStream (same idea)
// db.orders.aggregate([
// { $changeStream: { resumeAfter: lastToken } }
// ])
📤 Resume flow:
1. Process events, store event._id
2. On reconnect → resumeAfter: lastToken
3. Stream continues from next event
How It Works
Every change event includes a resume token in _id. Persist it (database, Redis, file) so workers survive restarts. Use startAfter when the old stream was invalidated.
Compare
📋 $changeStream vs related approaches
Approach
Effect
Best for
$changeStream
Push model—server streams change events
Live UI, sync, event-driven apps
db.collection.watch()
Helper around $changeStream + resume
Default choice in application code
Polling find()
Repeated queries for new/changed docs
Simple scripts; wasteful at scale
Oplog tailing (legacy)
Direct oplog read
Deprecated pattern—use change streams
$match on stream
Filter which events reach your app
Inserts-only feeds, namespace filters
🧠 How $changeStream Works
1
Open stream cursor
Client runs watch() or aggregate([{ $changeStream }]). MongoDB registers interest in the namespace.
Connect
2
Change committed
A write hits the collection. The oplog (replica set) records the operation.
Write
3
Event emitted
MongoDB builds a change event with operationType, optional fullDocument, and resume token _id.
Notify
=
📡
App reacts in real time
Your loop processes the event—update cache, push to WebSocket, enqueue a job.
Important
📝 Notes
Replica set required—standalone mongod returns an error when opening a change stream.
operationType values include insert, update, replace, delete, invalidate, and (with showExpandedEvents) DDL events.
Delete events include documentKey but usually no fullDocument.
An invalidate event closes the stream (e.g. collection dropped)—use startAfter to reopen.
Change streams see majority-committed changes—read concern affects what you observe.
$changeStream is MongoDB’s built-in way to subscribe to data changes. Open with watch(), filter with $match, enable updateLookup when you need full documents, and persist resume tokens for reliability.
Remember the replica set requirement before debugging “change stream not supported” errors. Next in the series: $collStats for collection statistics.
Prefer db.collection.watch() over raw aggregate() for resume support
Persist resume tokens durably before acknowledging event processing
Use $match early to reduce network traffic to your app
Set fullDocument: "updateLookup" when downstream needs the whole doc
Handle invalidate and reconnect with startAfter
❌ Don’t
Assume change streams work on standalone dev servers without replica set setup
Drop _id in $project if you rely on automatic resume
Process events without idempotency—retries may redeliver after reconnect
Leave streams open indefinitely without heartbeat/error handling in production
Poll with find() when push-based $changeStream fits the use case
Summary
Key Takeaways
Knowledge Unlocked
Five things to remember about $changeStream
Use these points when building reactive MongoDB apps.
5
Core concepts
📡01
Live feed
Push, not poll.
Basics
📚02
First stage
Pipeline rule.
Syntax
🔄03
Replica set
Required deploy.
Setup
📄04
updateLookup
Full doc on update.
Option
🛠05
Resume token
event._id saved.
Reliability
❓ Frequently Asked Questions
$changeStream opens a change stream on the collection (or database/cluster when configured). It must be the first stage in an aggregation pipeline and emits one event document per insert, update, replace, delete, or other supported change—ideal for live dashboards and sync jobs.
Always first: db.orders.aggregate([{ $changeStream: {} }, …]). You may add later stages such as $match to filter event types, but stages that remove the resume token (_id) can break automatic resume behavior.
No. Change streams require a replica set or sharded cluster so MongoDB can track the oplog. Local standalone installs need a single-node replica set for testing.
db.collection.watch([{ $match: … }]) is the recommended helper—it wraps an aggregation starting with $changeStream and handles resume tokens. Using aggregate([{ $changeStream: {} }]) directly is equivalent but you manage the cursor yourself.
For update operations, MongoDB includes the current majority-committed document in the fullDocument field (or null if deleted). Without it, updates often only show updateDescription with changed fields—not the whole document.
Save the _id resume token from the last event and pass resumeAfter: token on restart. Use startAfter when the stream was invalidated (e.g. collection drop). resumeAfter, startAfter, and startAtOperationTime are mutually exclusive.
Did you know?
Change streams were introduced in MongoDB 3.6 and largely replaced manual oplog tailing. You can also open a database-level stream with db.watch() or cluster-wide on admin with allChangesForCluster. See the official $changeStream docs.