A polling job repeatedly asks what changed. A change stream can instead provide database change events through a long-lived cursor. That cursor still needs a resume and processing policy when the application restarts or loses its connection. This reference targets MongoDB 8.0 on a replica set or sharded deployment; a standalone mongod does not satisfy the prerequisite. The complete mongosh fixtures were designed for an isolated single-node teaching replica set. A single node demonstrates the API but does not provide production high availability. The examples establish a stream before writing controlled events and close every owned stream afterward.
Choose the watched scope and event filter
A collection watch observes that collection's supported changes, while broader database or deployment watches have their own scope and privilege requirements. Select only the events the consumer needs with an allowed pipeline. The main fixture includes inserts and updates, excluding reset deletes that occur before the watch. An event's operationType distinguishes the action, documentKey identifies the affected document, and updateDescription can describe changed fields for an update.
Do not remove or rewrite the event's resume-token _id in a pipeline that must remain resumable. Keep scope and pipeline compatible when reopening a stream. A watch cursor is long-lived and can use connections while waiting, so account for consumer count and pool capacity. It is not an automatically durable application queue with arbitrary retention. Replica-set history and stream behavior define what can be resumed. Consumers should handle closure and invalidation according to the documented operation, including special rules around collection drops or renames.
Preserve resume tokens as opaque values
A change event carries an opaque token that can be supplied through resumeAfter under its documented contract. Store the token as the BSON value rather than parsing its internal string to invent a timestamp or sequence number. A token is not a business event identifier chosen by the application. Persist it with enough scope and configuration metadata to reopen the intended stream. MongoDB and drivers can also provide post-batch resume progress, which needs its own correct checkpoint use.
Checkpoint after the corresponding application effect is safely handled, not merely after a network read returned an event. Otherwise a crash can skip unfinished work. If effect and checkpoint are separate operations, think through every crash boundary. Reprocessing can be acceptable with idempotent effects, but a non-idempotent counter or external payment needs stronger design. Old history may no longer support a token after prolonged downtime. Define a recovery route, such as rebuilding from a suitable baseline, rather than claiming a saved token guarantees indefinite replay.
Interpret fullDocument lookup correctly
An insert event can include its inserted document. For updates, fullDocument: updateLookup requests a lookup of the current majority-committed document under the documented behavior. That document can reflect later changes, so it is not automatically an exact image of the document at the instant of the specific update event. updateDescription and any separately configured pre/post-image features answer different questions and have their own requirements.
The fixture performs only one controlled update after resuming, so its lookup quantity is three. Do not generalize that observation to every busy production collection. A document may be deleted before a lookup, and fullDocument availability needs explicit handling. Consumers should validate the event shape for the operation type instead of assuming every event has the same fields. If the use case needs exact historical state, choose the documented image configuration or a domain event design that captures the necessary data rather than silently treating a current lookup as an immutable audit record.
Coordinate processing, deduplication, and shutdown
Reopening a stream and application retries can lead to repeated processing of an event. Use a stable deduplication representation or idempotent business identifier according to the consumer design. Claiming a deduplication record and then separately applying an effect can fail between those steps, losing work after a crash. The practice uses a transaction for its local counter, processed-token record, and checkpoint so those database effects commit together. External side effects require their own idempotency or outbox strategy.
Do not mix event-emitter consumption and iterator consumption on the same Node driver stream without the documented ownership contract. Choose one consumer, limit in-flight processing, and close the stream during shutdown. Persist the last safely handled checkpoint before ending according to policy. Automatic driver resumption can help transport recovery but does not decide application correctness. Monitor lag, errors, and history-window risk. The finite fixtures use tryNext to establish the cursor before the controlled write, avoiding a lazy-start race that would miss the event and leave next waiting indefinitely.
MongoDB documentation and version notes
MongoDB Change Streams and Resume Tokens Examples
Each example shows the code, the output it produces and what each part does.
Resuming a change stream after an update
Run this complete script only against the MongoDB 8.0 teaching replica set. It creates an empty source collection, opens a filtered stream, and performs an initial tryNext to establish the cursor. After an insert, it reads the event and saves that event's opaque token. It closes the first stream, updates the document, and opens a new stream with resumeAfter. The resumed event is the update, with quantity three in this controlled lookup. The output prints operation and business fields, not the token's internal bytes. All long-lived stream resources close in finally.
function take(stream) {
const deadline = Date.now() + 5000;
while (Date.now() < deadline) { const event = stream.tryNext(); if (event) return event; }
throw new Error('Expected change event did not arrive');
}
const source = db.getSiblingDB('codevra_ref_changes').getCollection('source');
source.deleteMany({});
source.insertOne({ _id: 0, qty: 0 }); source.deleteOne({ _id: 0 });
const pipeline = [{ $match: { operationType: { $in: ['insert', 'update'] } } }];
const stream = source.watch(pipeline, { fullDocument: 'updateLookup', maxAwaitTimeMS: 100 });
let token;
try {
stream.tryNext();
source.insertOne({ _id: 1, qty: 2 });
const event = take(stream);
token = event._id;
print(JSON.stringify({ operation: event.operationType, id: event.documentKey._id, qty: event.fullDocument.qty }));
} finally { stream.close(); }
source.updateOne({ _id: 1 }, { $set: { qty: 3 } });
const resumed = source.watch(pipeline, { resumeAfter: token, fullDocument: 'updateLookup', maxAwaitTimeMS: 100 });
try {
const event = take(resumed);
print(JSON.stringify({ operation: event.operationType, id: event.documentKey._id, qty: event.fullDocument.qty }));
} finally { resumed.close(); }{"operation":"insert","id":1,"qty":2}
{"operation":"update","id":1,"qty":3}MongoDB Change Streams and Resume Tokens in Real Projects
Deduplicate change events with a transactional checkpoint
Process the same captured insert event twice while applying one local counter effect. The solution stores the opaque token as a checkpoint and uses its Extended JSON serialization only as a deduplication key. A transaction atomically checks and inserts that key, increments the counter, and stores the checkpoint. The second attempt sees the key and returns false without repeating the effect. This is a local database exercise under a replica-set prerequisite. It does not make an external notification exactly once. Production transaction retries and consumer restart logic also need deliberate handling; the fixture isolates the successful deduplication path and its atomic database boundary.
function take(stream) {
const deadline = Date.now() + 5000;
while (Date.now() < deadline) { const event = stream.tryNext(); if (event) return event; }
throw new Error('Expected change event did not arrive');
}
const demo = db.getSiblingDB('codevra_ref_changes');
const source = demo.getCollection('practice_source');
source.deleteMany({}); source.insertOne({ _id: 0 }); source.deleteOne({ _id: 0 });
const processed = demo.getCollection('processed');
processed.deleteMany({}); processed.insertOne({ _id: 'seed' }); processed.deleteOne({ _id: 'seed' });
demo.getCollection('counter').replaceOne({ _id: 'applied' }, { _id: 'applied', count: 0 }, { upsert: true });
demo.getCollection('checkpoint').deleteMany({});
const stream = source.watch([{ $match: { operationType: 'insert' } }], { maxAwaitTimeMS: 100 });
let event;
try { stream.tryNext(); source.insertOne({ _id: 1 }); event = take(stream); }
finally { stream.close(); }
const session = db.getMongo().startSession();
const transactional = session.getDatabase('codevra_ref_changes');
function handle(event) {
const key = EJSON.stringify(event._id);
session.startTransaction();
try {
if (transactional.processed.findOne({ _id: key })) { session.commitTransaction(); return false; }
transactional.processed.insertOne({ _id: key });
transactional.counter.updateOne({ _id: 'applied' }, { $inc: { count: 1 } });
transactional.checkpoint.updateOne({ _id: 'watch' }, { $set: { token: event._id } }, { upsert: true });
session.commitTransaction(); return true;
} catch (error) { session.abortTransaction(); throw error; }
}
try { print(handle(event)); print(handle(event)); }
finally { session.endSession(); }
print(demo.counter.findOne({ _id: 'applied' }).count);true
false
1MongoDB Change Streams and Resume Tokens FAQ
Can I run watch on a standalone server?
Not for the demonstrated change-stream feature. Use a replica set or supported sharded deployment. The local verification uses an isolated single-node replica set, which satisfies the teaching prerequisite but does not provide production redundancy. Connect to the intended deployment explicitly. A failed standalone example should not be fixed by silently changing to an existing user database or an unrelated remote cluster.
Should I decode the token into my own timestamp?
No. Preserve it as an opaque value for the documented resume API. Its internal representation is not your application sequence contract. Store the scope and configuration needed for resumption as well. Checkpoint after safe processing, and define recovery if the history needed for that token has expired. A saved token supports resumption under conditions; it is not an unlimited permanent event archive.
Is updateLookup an exact historical after-image?
Not necessarily. It looks up the document under the documented current committed behavior and can reflect later changes or an absent document. The controlled fixture has one update, so its quantity is deterministic. A busy consumer must handle the actual event shape and choose pre/post-image or domain-event facilities when exact historical state is required. Do not use a current lookup as an unsupported audit guarantee.
Why transactionally store the checkpoint and local effect?
Separate writes can leave a crash boundary where the checkpoint advances without the effect, or the effect repeats without a checkpoint. The practice commits its counter, deduplication record, and token together. That protects these local database changes under the stated transaction prerequisites. External effects still need idempotency or an outbox-like design, and production retries need a documented policy rather than assuming every consumer execution is unique.
MongoDB Change Streams and Resume Tokens Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What counter value remains after the worked handler receives the same event twice?
MongoDB Change Streams and Resume Tokens Quiz
2 questions to check what you learned.
Why call tryNext before the controlled insert in the fixture?
- To establish the stream before generating the event
- To delete all previous source records automatically
- To make the stream durable forever
When should the application checkpoint an event?
- After its corresponding effect is safely handled under the chosen policy
- Immediately when the network delivers it regardless of processing
- Only by decoding the token and guessing a timestamp