MongoDB $changeStream Stage

Beginner
5 min read
Updated: Oct 2026
3 examples
Live preview

What Is $changeStream?

$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
UsePrefer
Live inserts / updates / deletes$changeStream / watch()
Filter which events you get$match after the stream
One-shot query of current datafind() / normal aggregation

How to Use $changeStream in MongoDB

Confirm replica set mode, open a stream, then react to each event.

  1. Run on a replica set or sharded cluster (rs.status()).
  2. Open with db.orders.watch() (or aggregate([{ $changeStream: {} }])).
  3. Optionally filter with $match and set options like fullDocument.
  4. Read events with .next() / .hasNext() and save event._id to resume later.
PieceWhereRole
watch([pipeline], options)HelperOpens stream; pipeline filters events
$changeStreamFirst aggregate stageExplicit stream stage + options
fullDocumentOption / stage fieldupdateLookup loads full doc after update
resumeAfterOption / stage fieldContinue from a saved token

Pseudocode

Pseudocode
require replica set (or sharded cluster)

stream = openChangeStream(collection, options?)

loop:
  wait for next committed write
  event = {
    _id: resumeToken,
    operationType: insert | update | delete | …,
    documentKey, ns, fullDocument?, …
  }
  if pipeline has $match → keep or drop event
  app.handle(event)
  save(event._id)   // for resumeAfter later

Cheat Sheet

Keep these facts handy while building reactive MongoDB apps.

TopicDetail
TypeAggregation stage (tailable cursor)
PositionMust be first in aggregate()
DeploymentReplica set or sharded cluster
Helperdb.coll.watch([ … ], { … })
Key fieldsoperationType, fullDocument, documentKey, ns
ResumeSave event._id → resumeAfter
InvalidateStream closes (e.g. drop) → use startAfter

Live Preview

Simulate a change stream: pick an operation and see a simplified event shape (teaching model—not a live MongoDB connection).

Collection: orders. Update events show fullDocument as if updateLookup were on.

Result
operationType: "insert"
ns: { db: "test", coll: "orders" }
documentKey: { _id: 1 }
fullDocument: { _id: 1, product: "Mouse", amount: 25 }
_id: { _data: "<resume-token>" }

Worked Walkthrough — One Insert Event

Collection orders already has a Keyboard row. You open a stream, then insert a Mouse order in another session.

Calculation
1. App opens:  db.orders.watch()
2. Write:      insert { product: "Mouse", amount: 25 }
3. MongoDB commits the write to the replica set oplog
4. Stream emits one event:
     operationType: "insert"
     fullDocument:  { product: "Mouse", amount: 25, … }
     _id:           resume token (save this)

No polling. The cursor waited, then delivered the event.

MongoDB Examples

We use a small orders collection. Open a stream with watch(), then filter inserts. Run these in mongosh on a replica set.

Example 1 — Sample Data

Confirm replica set mode, then seed one order. Open the change stream in a second mongosh session when you try Examples 2–3.

mongosh
// Must be a replica set (or sharded cluster)
rs.status()

db.orders.drop()
db.orders.insertOne({
  _id: 1,
  product: "Keyboard",
  status: "pending",
  amount: 45
})

How the Data Is Stored in MongoDB

After the insert, MongoDB keeps one document in orders. Run find() to see it:

mongosh
db.orders.find()

Read the important fields like this:

FieldExamplePlain English
product"Keyboard"Item ordered
status"pending"Order state (we update this later)
amount45Order total

How It Works

  1. Check deployment. rs.status() confirms replica set mode—required for change streams.
  2. Seed one document. A starting order gives you something to update in later demos.
  3. 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: {} }
// ])

How It Works

  1. Open the cursor. watch() starts listening; it blocks until a change arrives.
  2. Write in another session. The insert becomes a committed oplog entry.
  3. Read the event. stream.next() returns one change document with operationType and fullDocument.
  4. 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" } }
// ])

How It Works

  1. Pipeline filters events. Stages after the stream act on each change document.
  2. $match on operationType. Updates never reach your app on the inserts-only cursor.
  3. Options vs stage. Pass fullDocument as the second argument to watch(), or inside { $changeStream: { … } }.
  4. 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).

Performance Notes

ApproachCostBest for
watch() / $changeStreamPush; waits on oplogLive UI, sync, triggers
Stream + $matchFewer events over the wireInserts-only or filtered feeds
Polling find()Repeated queriesTiny scripts only

Filter early with $match so your app only receives useful events. Prefer change streams over tight polling loops on busy collections.

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.

Next: $collStats

Inspect collection size, storage, and index stats in the aggregation pipeline.

$collStats tutorial →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

9 people found this page helpful