Applications often need to react when data changes in a database. An order-processing service might need to update an inventory system after an order is created. A reporting application might refresh its dashboard when a record changes. A notification service might send an alert when a particular document is updated.
One approach is to repeatedly query the database to check whether anything has changed. However, polling can generate unnecessary database traffic, introduce delays, and require additional logic to determine which records have already been processed.
MongoDB Change Streams provide another option. They allow applications to subscribe to changes in a collection, database, or deployment and receive change events as they occur.
However, a long-running change stream can encounter network interruptions, server restarts, or temporary connectivity failures. If the application does not handle these interruptions correctly, it may stop receiving events or process the same event more than once.
MongoDB provides resume tokens that help applications continue watching changes from a previously observed position. Understanding how to capture these tokens, persist them safely, and handle recovery is essential for building reliable change-stream consumers.
This article explains how MongoDB Change Streams work, how to implement them in Node.js, and how to resume event processing after a disconnect.
What Are MongoDB Change Streams?
MongoDB Change Streams allow applications to observe database changes through a stream of events. Instead of repeatedly querying for updated records, an application opens a change stream and processes events as MongoDB reports them.
Change streams can monitor changes at different scopes:
Collection level: Watches changes to a specific collection.
Database level: Watches changes across collections in a database.
Deployment level: Watches changes across databases in a supported MongoDB deployment.
For example, an inventory service can watch the orders collection for inserts and updates. When an order changes, the application receives an event describing the operation and relevant document information.
A change event may contain information such as the operation type, namespace, document key, and the document's updated fields, depending on the event type and the options used to open the stream.
Change streams are built on MongoDB's change notification infrastructure and require a replica set or a supported sharded cluster. They are not available on a standalone MongoDB server in the same way.
How Resume Tokens Work
A resume token is a value MongoDB provides with change events. Applications can use it to identify a position in the change stream and request that MongoDB resume watching from that position.
Consider a consumer that processes events in this order:
MongoDB reports an order creation event.
The application processes the event.
The application saves the event's resume token.
The application receives the next event.
If the application disconnects later, it can use the saved token to resume the stream rather than starting from the beginning.
Resume tokens are opaque identifiers. Applications should store and pass them back to MongoDB without trying to interpret their internal structure.
A token is not a substitute for application-level event tracking. It helps MongoDB locate a position in the stream, but it does not automatically guarantee that an external operation was completed exactly once.
Why saving the token matters
Imagine that an application processes a change event and then crashes before saving its resume token. After restarting, it may resume from an older token and receive the same event again.
That behavior is expected. The application needs to handle duplicate processing safely.
There is also a more serious case: the application saves the token before completing the business operation. If it crashes afterward, it may resume after the event even though the operation was never completed.
For reliable processing, the application must coordinate event processing and checkpoint persistence carefully.
Setting Up a Change Stream in Node.js
The following example uses the official MongoDB Node.js driver.
Install the package:
npm install mongodb
The example assumes MongoDB is running as a replica set or on a supported sharded cluster and that the configured user has permission to watch the relevant collection.
Step 1: Connect to MongoDB
Create a file named change-stream.js.
const { MongoClient } = require("mongodb");
const uri = process.env.MONGODB_URI;
if (!uri) {
throw new Error("MONGODB_URI is required.");
}
const client = new MongoClient(uri);
async function main() {
await client.connect();
const database = client.db("store");
const orders = database.collection("orders");
console.log("Connected to MongoDB");
await watchOrders(orders);
}
main()
.catch((error) => {
console.error("Application failed:", error);
process.exitCode = 1;
})
.finally(async () => {
await client.close();
});
The connection string is loaded from an environment variable rather than being embedded in the source code.
For a local replica-set deployment, the URI must include the appropriate replica-set configuration. For a managed MongoDB deployment, use the connection string and authentication settings provided for that environment.
The watchOrders function will be implemented in the next step.
Step 2: Open a change stream
A collection exposes a watch() method that returns a change stream.
async function watchOrders(orders) {
const changeStream = orders.watch();
try {
for await (const change of changeStream) {
console.log("Change received:", {
operationType: change.operationType,
documentKey: change.documentKey,
resumeToken: change._id
});
}
} finally {
await changeStream.close();
}
}
The for await...of loop consumes change events asynchronously as they arrive.
The _id field in a change event contains its resume token. Although _id is also a common field name for MongoDB documents, the _id on the change event represents the event's resume token.
This basic example demonstrates event consumption, but it does not yet handle persistent checkpoints or reconnection. If the stream closes unexpectedly, the loop may terminate with an error.
For production applications, the consumer needs an explicit recovery strategy.
Persisting Resume Tokens
A resume token must survive application restarts if the application is expected to continue from a previously processed position.
A common approach is to store the token in a separate checkpoint collection.
For example, the application might maintain a document like this:
{
"_id": "orders-consumer",
"resumeToken": {
"_data": "opaque-token-value"
}
}
The token shown here is illustrative, not a valid token to use in a real deployment.
The checkpoint collection stores the most recently committed resume position for the consumer. When the application starts, it reads the checkpoint and passes the saved token to the change-stream API.
Step 3: Load and save the checkpoint
The following helpers read and write the checkpoint.
async function loadCheckpoint(checkpoints, consumerId) {
const checkpoint = await checkpoints.findOne({
_id: consumerId
});
return checkpoint?.resumeToken ?? null;
}
async function saveCheckpoint(
checkpoints,
consumerId,
resumeToken
) {
await checkpoints.updateOne(
{ _id: consumerId },
{
$set: {
resumeToken,
updatedAt: new Date()
}
},
{ upsert: true }
);
}
The consumerId identifies the logical consumer whose progress is being tracked.
Use a stable identifier for a particular processing workflow. If multiple independent consumers share the same checkpoint, one consumer could overwrite another consumer's progress.
The token is stored as BSON-compatible data rather than being manually parsed or reconstructed. MongoDB's driver returns the token in a form that can be passed back to the change-stream API.
In production, restrict access to the checkpoint collection and include the appropriate durability and backup considerations for your workload.
Resuming a Change Stream After a Restart
MongoDB's change-stream API supports resume options that allow a consumer to continue from a saved position.
The resumeAfter option is used with a previously obtained resume token. The application can pass the token to watch() when opening a new stream.
Step 4: Open the stream from the saved token
async function openOrdersStream(orders, resumeToken) {
const options = {};
if (resumeToken) {
options.resumeAfter = resumeToken;
}
return orders.watch([], options);
}
When a token is available, the application requests that MongoDB resume after that position.
When no token is available, the application opens a new stream without a resume option. The default starting position is determined by MongoDB's change-stream behavior at the time the stream is opened.
That distinction matters for initial deployment. If the application must process historical data as well as future changes, a change stream alone is not a replacement for an explicit backfill or snapshot strategy.
Step 5: Process events and save checkpoints
The next example combines event processing with checkpoint persistence.
async function consumeOrders(
orders,
checkpoints,
consumerId
) {
const resumeToken = await loadCheckpoint(
checkpoints,
consumerId
);
const changeStream = await openOrdersStream(
orders,
resumeToken
);
try {
for await (const change of changeStream) {
await processOrderChange(change);
await saveCheckpoint(
checkpoints,
consumerId,
change._id
);
}
} finally {
await changeStream.close();
}
}
This example saves the resume token after the event handler succeeds.
That order is important: the application does not advance its checkpoint before attempting the business operation.
However, the code still has a failure window. If processOrderChange() completes its external side effect and the application crashes before saveCheckpoint() finishes, the event may be delivered again.
Therefore, checkpointing after processing provides at-least-once-style recovery behavior, but it does not guarantee that an external side effect occurs only once.
The event handler should be idempotent, or the business operation and checkpoint should be coordinated through a suitable transactional design.
Handling Disconnects Automatically
A long-running application should not assume that a change stream remains open forever.
Temporary network failures, primary elections, and other interruptions can cause a stream to close or report an error. MongoDB drivers handle some resumable errors automatically, but applications still need to handle failures that escape the driver's retry behavior.
A practical consumer can reopen the stream after an error, using the most recently saved token.
async function runConsumer(
orders,
checkpoints,
consumerId
) {
let delayMs = 1000;
while (true) {
try {
await consumeOrders(
orders,
checkpoints,
consumerId
);
delayMs = 1000;
} catch (error) {
console.error(
"Change stream failed:",
error.message
);
await new Promise((resolve) =>
setTimeout(resolve, delayMs)
);
delayMs = Math.min(delayMs * 2, 30000);
}
}
}
This loop demonstrates exponential backoff: the retry delay increases after repeated failures, up to 30 seconds.
It is a starting point rather than a complete production implementation. A robust consumer should also support cancellation, distinguish resumable errors from permanent configuration failures, and avoid retrying indefinitely when the checkpoint can no longer be resumed.
For example, an invalid database credential should not be treated the same way as a brief network interruption.
The consumer should also ensure that its shutdown process stops the retry loop and closes active streams cleanly.
Choosing Between resumeAfter, startAfter, and startAtOperationTime
MongoDB provides several options for selecting where a change stream begins.
Option | Purpose | Important consideration |
|---|
resumeAfter
| Continues after a previously observed resume token | The token must still be usable within the available change history |
startAfter
| Starts after a resume token and can be used to continue after certain invalidation events | It is not interchangeable with resumeAfter in every situation |
startAtOperationTime
| Starts at a specified operation time | The requested time must be available in the retained change history |
For most ordinary disconnect-recovery workflows, resumeAfter is the natural choice.
startAfter is useful in scenarios where the stream has been invalidated, such as certain collection or database lifecycle operations. The exact behavior depends on the event and the MongoDB version.
startAtOperationTime is useful when an application needs to begin from a known logical point in time rather than from a previously saved event token.
Do not switch between these options casually. Select the option that matches the recovery requirement and verify the behavior against the MongoDB version used by the application.
What Happens When a Resume Token Is Too Old?
A resume token is useful only while MongoDB retains enough change history to resume from the requested position.
If the application remains offline longer than the available oplog history, MongoDB may no longer be able to resume from the saved token.
This can happen when the source deployment has a high write volume, the oplog window is short, or the consumer remains disconnected for an extended period.
When this occurs, repeatedly retrying the same token will not restore the missing history.
The application needs a recovery plan.
Option 1: Rebuild the consumer state
If the application maintains a derived view or cache, it may be possible to rebuild that state from the current source collections and then start a new change stream.
However, taking a snapshot and starting a stream without coordinating their boundaries can introduce gaps or duplicate updates. Use a documented snapshot-and-streaming strategy that establishes a consistent transition between the initial data load and ongoing changes.
Option 2: Perform a controlled resynchronization
For a downstream database or search index, the application might reload the affected dataset and establish a new checkpoint after the resynchronization process completes.
The exact approach depends on the consistency requirements and whether the consumer can tolerate temporary staleness.
Option 3: Increase available change history
Where appropriate, adjust oplog capacity or operational configuration to retain enough history for expected consumer outages.
This reduces the likelihood of a token becoming unusable, but it does not eliminate the need for recovery procedures.
Common Mistakes and Troubleshooting
Events are processed more than once
Check whether the application saves its checkpoint only after successful processing. A crash between the business operation and checkpoint update can cause redelivery.
Make the handler idempotent and use stable event or document identifiers to prevent duplicate side effects.
The consumer misses changes after restarting
Verify that the application loads the correct checkpoint and passes it through the appropriate resume option.
Check whether the token was overwritten by another consumer or saved before the associated operation completed.
Also confirm that the token is still within the available change history.
MongoDB cannot resume from the token
The oplog may no longer contain the required history, or the token may be invalid for the requested stream.
Inspect the error and determine whether the problem is temporary or requires resynchronization. Do not automatically discard the checkpoint and start from the present if doing so could silently lose required events.
The change stream does not report the expected update
Confirm that the stream is watching the correct collection or database and that the relevant operation falls within the configured pipeline.
By default, an update event does not necessarily include the complete post-update document. If the application needs the document after an update, consider using the appropriate full-document option and understand its consistency implications.
The application becomes slow under heavy writes
Inspect event-processing latency, queue growth, database load, and the time spent saving checkpoints.
If event handling is slower than the incoming change rate, the consumer may fall behind. Optimize the handler, reduce unnecessary database queries, and consider bounded concurrency where event ordering and consistency requirements permit it.
Best Practices for Production
Save tokens durably
Use persistent storage for resume tokens. An in-memory token disappears when the process restarts and therefore cannot support reliable recovery across restarts.
Coordinate processing and checkpoint updates
Persist the token only after the corresponding business operation succeeds.
If the business data and checkpoint are stored in the same MongoDB deployment, consider whether a transaction can atomically commit both changes. A transaction is not always necessary or suitable, but it can eliminate certain failure windows when the application architecture supports it.
For external systems, use idempotency keys, deduplication records, or an outbox/inbox design as appropriate.
Monitor consumer lag
Track how far behind the source the consumer is, how long event processing takes, how often streams resume, and how frequently events are retried.
MongoDB does not provide a universal, exact lag metric for every change-stream workflow, so applications may need to measure processing delay using event metadata and their own timestamps.
Use bounded concurrency
Parallel event processing can improve throughput, but it can also change completion order.
If events for the same document or business entity must be applied sequentially, preserve the required ordering. Use bounded concurrency and partitioning strategies only when they are compatible with the application's consistency model.
Plan for initial synchronization
A new consumer without a resume token needs a defined starting behavior.
If it must process all relevant historical records, implement a backfill or snapshot process rather than assuming the change stream will automatically replay the entire collection.
Handle shutdown cleanly
Stop accepting new work when the application receives a termination signal, allow in-flight operations to finish within a defined timeout, and close the change stream and database client.
A clean shutdown reduces unnecessary retries and makes checkpoint behavior easier to reason about.
Conclusion
MongoDB Change Streams provide a practical way to react to database changes without continuously polling collections. Resume tokens allow applications to continue watching changes after a disconnect or restart, provided the required change history remains available.
Reliable recovery depends on more than saving a token. The application must coordinate checkpoint persistence with business processing, tolerate duplicate events, handle errors appropriately, and define a recovery strategy for tokens that can no longer be resumed.
For production applications, combine durable checkpoints, idempotent event handlers, monitoring, controlled retries, and a documented resynchronization process. These practices help maintain reliable downstream processing even when the application or database connection is interrupted.
Join the conversation! Your thoughts help the community grow.