Opening a Change Stream on a Collection
Learners will call watch() on a collection and consume the event stream in a Node.js application using async iteration.
Opening a Change Stream on a Collection is a free MongoDB Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the MongoDB Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Are Change Streams?
Change streams provide a real-time event feed of all insert, update, replace, delete, and invalidate operations on a MongoDB collection, database, or entire deployment. Introduced in MongoDB 3.6, they are built on top of the oplog (operation log)—the replica set's replication journal—but expose a high-level, resumable cursor API instead of requiring you to parse the raw oplog format.
Prerequisites for Change Streams
Change streams require a replica set or sharded cluster—they do not work on standalone MongoDB instances because they depend on the oplog. On MongoDB Atlas, all clusters (even the free tier M0) are replica sets, so change streams work out of the box. For local development, you need to start mongod with --replSet rs0 and initiate the replica set with rs.initiate() in mongosh.
// Verify you're on a replica set before using change streams
// In mongosh:
rs.status() // should show replica set members, not an error
// If running locally without a replica set, start one:
// mongod --replSet rs0 --port 27017 --dbpath /data/db
// Then in mongosh: rs.initiate()Opening a Change Stream With watch()
Call collection.watch() to open a change stream on a specific collection. The method returns a ChangeStream cursor object that you can iterate using async iteration, the next() method, or event listeners. The stream stays open and delivers events as they occur. An empty watch() call with no pipeline receives all change event types on that collection.
const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGODB_URI);
async function watchCollection() {
const db = client.db('ecommerce');
const orders = db.collection('orders');
// Open a change stream on the orders collection
const changeStream = orders.watch();
console.log('Watching for changes...');
// The stream is now open and ready to deliver events
return changeStream;
}Consuming Events With Async Iteration
The most readable way to consume change stream events in modern Node.js is with async iteration using for await...of. This syntax automatically handles the cursor's next() calls and pauses the loop between events. The loop runs indefinitely until the change stream is closed or an error occurs. Always wrap the loop in a try/finally to ensure the stream is closed when your process exits.
async function processChanges() {
const changeStream = db.collection('orders').watch();
try {
for await (const change of changeStream) {
console.log('Change received:', change.operationType);
console.log('Document key:', change.documentKey._id);
// Handle the change event
await handleOrderChange(change);
}
} finally {
await changeStream.close();
}
}Consuming Events With EventEmitter
Alternatively, you can use the change stream as a Node.js EventEmitter. This approach is familiar if you use streams elsewhere in your code. Register a 'change' event handler for normal events and an 'error' handler for connection issues. This style is useful when you want to react to events without blocking a function with an await loop.
const changeStream = db.collection('products').watch();
changeStream.on('change', (event) => {
console.log('Product changed:', event.operationType, event.documentKey._id);
if (event.operationType === 'update') {
invalidateProductCache(event.documentKey._id);
}
});
changeStream.on('error', (error) => {
console.error('Change stream error:', error);
// Implement reconnect logic or close gracefully
});
// Close when done
// changeStream.close();Change Stream Scope: Collection, Database, Client
You can open change streams at three scopes: collection level (watches one collection), database level (watches all collections in a database), and client level (watches all databases and collections in the deployment). The broader the scope, the higher the event volume. Most applications watch specific collections to receive only the events they care about.
// Collection-level (most common)
const stream1 = db.collection('orders').watch();
// Database-level — all collections in 'ecommerce'
const stream2 = client.db('ecommerce').watch();
// Client-level — everything in the deployment
const stream3 = client.watch();
// Events at db/client scope include 'ns' field to identify which collection changed
for await (const change of stream2) {
console.log('Changed collection:', change.ns.coll);
}Change Stream Options: fullDocument
By default, update events only include the fields that changed (the update description), not the entire document. If you need the full updated document in the event payload, pass { fullDocument: 'updateLookup' } to watch(). This causes MongoDB to perform an additional lookup of the document after the update and include it in the change event. Be aware this adds latency and is a separate read after the event.
// Receive the full document in update events
const changeStream = db.collection('users').watch(
[], // empty pipeline = all events
{ fullDocument: 'updateLookup' }
);
for await (const change of changeStream) {
if (change.operationType === 'update') {
// change.fullDocument is now the complete updated user document
console.log('Updated user:', change.fullDocument.email);
await syncToSearchIndex(change.fullDocument);
}
}Real-Time Dashboard Use Case
A classic use case for change streams is powering a live dashboard that shows new orders as they arrive. When a new order document is inserted, the change stream triggers, and your Node.js backend can push the update to connected clients via WebSocket or Server-Sent Events. This eliminates polling and provides true real-time updates without overloading the database with repeated queries.
// Server: push new orders to dashboard clients via WebSocket
async function startOrderWatcher(io) { // io = socket.io instance
const changeStream = db.collection('orders').watch([
{ $match: { operationType: 'insert' } }
]);
for await (const change of changeStream) {
const newOrder = change.fullDocument;
// Broadcast to all connected dashboard clients
io.to('dashboard').emit('newOrder', {
id: newOrder._id,
customer: newOrder.customerId,
amount: newOrder.total,
timestamp: newOrder.createdAt
});
}
}Event-Driven Microservices With Change Streams
Change streams can replace a message broker like Kafka or RabbitMQ for simple event-driven microservice patterns. When Service A writes to MongoDB, Service B watches the collection and reacts to changes. This avoids the operational complexity of a separate message bus for low-to-medium event volumes. However, for high-throughput use cases, dedicated message brokers offer better guarantees and higher throughput than change streams.
// Inventory service reacts to confirmed orders
async function inventoryWatcher() {
const changeStream = db.collection('orders').watch([
{
$match: {
operationType: 'update',
'updateDescription.updatedFields.status': 'confirmed'
}
}
], { fullDocument: 'updateLookup' });
for await (const change of changeStream) {
const order = change.fullDocument;
for (const item of order.items) {
await reserveInventory(item.productId, item.quantity);
}
}
}Closing a Change Stream Gracefully
Change streams consume a persistent connection to the MongoDB server. Always close them when your process shuts down or when you no longer need the stream. Call changeStream.close() which returns a Promise. For long-running applications, listen for process signals (SIGTERM, SIGINT) and close streams and clients before exiting to avoid resource leaks on the server side.
const changeStream = db.collection('events').watch();
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('Shutting down...');
await changeStream.close();
await client.close();
process.exit(0);
});
// Handle SIGINT (Ctrl+C in development)
process.on('SIGINT', async () => {
await changeStream.close();
await client.close();
process.exit(0);
});Change Streams vs Polling
Before change streams, applications relied on polling—periodically querying MongoDB to detect new or updated documents. Polling wastes resources (queries run even when nothing changed), introduces latency (the detection delay equals the poll interval), and scales poorly under high event rates. Change streams eliminate polling entirely: your application is notified immediately when data changes, consuming minimal resources when the collection is quiet. This makes change streams the preferred pattern for any feature that needs to react to data changes.
Quick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: change streams provide a real-time event feed of all CRUD operations on a collection, database, or deployment, they require a replica set and are consumed via async iteration or EventEmitter, and the fullDocument option requests the complete updated document in update events. Next up we explore the structure of change event documents and how to handle each operation type.
Frequently asked questions
Is the “Opening a Change Stream on a Collection” lesson free?
Yes — the full text of “Opening a Change Stream on a Collection” is free to read here on the web, and the MongoDB Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the MongoDB Academy course, upgrade to CoddyKit PRO.
What will I learn in “Opening a Change Stream on a Collection”?
Learners will call watch() on a collection and consume the event stream in a Node.js application using async iteration. You practise MongoDB Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start MongoDB Academy?
No prior experience is required. MongoDB Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Opening a Change Stream on a Collection” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this MongoDB Academy lesson?
Yes. Every MongoDB Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Opening a Change Stream on a Collection
- Change Event Document Structure
- Filtering Events With an Aggregation Pipeline
- Resuming Change Streams After Interruption