Est.

MongoDB Change Streams Architecture and CDC Limitations

Resume tokens protect change streams only until the oplog overwrites them.

· 10 min read
Cover illustration for “MongoDB Change Streams Architecture and CDC Limitations”
Change Data Capture · October 10, 2026 · 10 min read · 2,339 words

MongoDB change streams give application code a structured, resumable way to watch a database for changes, built on a mechanism that is not new. Streamkap's CDC guide compares the effect to enabling push notifications instead of constantly refreshing a page: the oplog exists whether or not anyone is using it for CDC, so tapping into it does not create new write-path cost.

What change streams add is translation. Rather than exposing the raw internal oplog format, which is an implementation detail MongoDB has changed across versions and never intended for external consumption, the API wraps each change in a structured envelope: an operation type (insert, update, replace, delete), the namespace affected, a document key, the document itself when requested, and an opaque resume token. That translation is what makes the interface usable by application developers who would otherwise need intimate knowledge of MongoDB's internal replication format just to track changes to a collection.

The resume token is the single mechanism that makes change streams fault-tolerant, and it deserves attention at the conceptual level before anything else. A consumer stores the token after processing a batch successfully, and if the consumer restarts, it opens a new stream from that persisted token rather than from whatever position happened to be sitting in memory. It is tempting to treat the resume token as a complete answer to restarts. It is not quite that simple, and the conditions under which it breaks down are the subject of nearly everything that follows in this piece. For now, the point to hold onto is structural: change streams are a lens on the oplog, not a separate system, and every constraint that governs the oplog governs change streams too.

The infrastructure prerequisites change streams impose before a single event flows

Before a single change event can flow, the deployment itself has to meet requirements that are easy to overlook when a CDC project is still in the scoping stage. Change streams only work on replica sets and sharded clusters running the WiredTiger storage engine. Standalone MongoDB instances do not support them in any form. That sets a hard floor: a change-stream-based pipeline requires, at minimum, a replica set, not a single server.

For teams running on MongoDB Atlas, this requirement is effectively invisible, since Atlas deployments always run as replica sets by default. Provisioning additional nodes, configuring replication, and taking on the operational responsibility of keeping a replica set healthy is a real infrastructure project in its own right, one that has to be budgeted and staffed before any CDC work begins.

MongoDB does not support indexes on the oplog collection, so every open change stream adds server-side processing load as it scans for relevant entries. The constraints that matter more, and that shape almost everything about how a production pipeline has to be built, come from the oplog's size, not from how many streams are reading it.

How the oplog's capped size limits pipeline resilience

Diagram: Why the Resume Token Is a Conditional Guarantee. Visualizes: Show the life cycle of a change stream consumer failure and the narrow window in which recovery is possible versus impossible.

The oplog is capped: it has a fixed size, and old entries get overwritten as new writes arrive. That single fact is the source of the most consequential limitation in the entire change streams architecture. If a CDC consumer falls behind, whether because of a restart, a replay operation, a sudden burst of upstream writes, or simple processing delay, the resume token it's holding can end up pointing to an oplog entry that has already been overwritten. At that point resumption is not just difficult, it's impossible, and the gap in captured changes becomes permanent data loss.

Expedia Group's engineering team documented exactly this failure mode in production. Running CDC against an 18-machine Apache Ignite cluster, the team found that during replay operations the oplog size was not sufficient to hold all pending updates, forcing a restart of the change streams after the replay just to re-sync the cache with the underlying documents. Their engineering blog states the consequence directly: the token no longer exists in the oplog, the record has been discarded, and depending on the use case, data loss results. The team's workaround was to open a new stream pointing to five minutes before the previous subscriber died, a defensive buffer meant to catch any events that might have slipped through in the gap.

That example reveals the resume token for what it actually is: a durability guarantee that holds only conditionally. The token promises resumption, but that promise is backed by the oplog entry still being there to resume from. Nothing about the API prevents that entry from being overwritten if the consumer takes too long to come back.

The practical consequence is that oplog window sizing cannot be left at whatever default a deployment ships with. It has to be treated as a CDC pipeline design parameter, sized against the worst case a team can reasonably expect: the time it takes to detect that a consumer has failed, the time it takes to triage the failure, and the time it takes to get a replacement consumer running again, all added together and covered with margin. Get that calculation wrong, and the pipeline has a hidden expiration date on its own recoverability.

Collection drops add a related trap. Dropping or renaming a collection triggers an invalidate event that closes the stream's cursor, and MongoDB's own documentation is explicit that a stream cannot be resumed with resumeAfter once that invalidate event has fired. A new stream can be opened using startAfter instead, but any changes that occurred between the last event the pipeline actually captured and the moment of the drop are gone for good. There is no recovery path for that window, only the option to start fresh.

What update events contain by default

Update events carry less than most newcomers to the API assume. By default, an update event in a change stream does not include the full document as it looks after the change. A consumer that receives an update event and wants to know the complete current state of that document has to fetch it separately or change how the stream is configured, because the event alone doesn't carry that information.

The fullDocument: 'updateLookup' option solves part of this by instructing MongoDB to perform a secondary read and attach the document's current state to the event. Streamkap's documentation for both its CDC guide and its MongoDB-to-Snowflake guide treats this as something a team must configure explicitly; it is not behavior that happens automatically.

MongoDB 6.0 introduced a complementary capability, fullDocumentBeforeChange, which captures the pre-image of a document as it looked before a write was applied. That addresses a different problem than updateLookup does. There is no way to retrofit pre-image capture onto a stream that is already running against a collection that wasn't configured for it from the start.

One further limit applies regardless of configuration. The 16 MB BSON document size limit applies to the change stream notification itself, not only to the stored document. MongoDB's response, the $changeStreamSplitLargeEvent aggregation stage introduced in version 7.0 and back-ported to 6.0.9, splits oversized events into fragments so they can be delivered within the limit. Using it means a consumer now has to reassemble those fragments correctly before the event means anything, which is additional logic that has to be built, not a problem that disappears once the stage is enabled.

None of this is a bug to report. A pipeline built without that knowledge will run, and it will silently produce incomplete or slightly stale records on every single update it processes, with no error raised anywhere to say so.

Sharded clusters and the latency penalty

Sharded clusters add an architectural constraint that no configuration change can fix. Keeping a change stream globally ordered across a sharded cluster requires the mongos router to check in with every shard for every change notification, confirming whether that shard has seen anything more recent. That coordination round-trip adds latency that scales with the number of shards involved and with how far apart those shards are distributed geographically.

This is a structural trade-off built into the design, not a setting that can be tuned away. The coordination overhead is the cost of the global ordering guarantee the cluster-wide stream provides. Teams that don't need that guarantee across the whole cluster can scope their streams to individual collections or individual shards instead, which avoids the coordination penalty, but at the cost of losing any ordering guarantee that spans across shards. The decision is whether a given application's freshness and ordering requirements can tolerate the latency that comes with keeping that guarantee intact across a geographically distributed cluster, and that latency has to be modeled before the architecture is committed to, not discovered after it's in production.

MongoDB's schema flexibility, which makes development fast, makes CDC harder

MongoDB's appeal for application development comes largely from its lack of an enforced schema, and that same quality is what makes replicating its data into a structured destination genuinely harder than relational CDC. MongoDB has no equivalent guarantee. Its schema is emergent from whatever documents happen to exist in a collection. Two documents sitting in the same collection can carry entirely different sets of fields, and that structure can shift at any moment with nothing resembling a migration event to announce it.

Replicating that into a columnar destination like Snowflake, BigQuery, or Redshift forces a choice that has to be made deliberately. Streamkap's MongoDB-to-Snowflake guide names schema mapping as the central challenge in this kind of pipeline precisely because MongoDB's schema-on-read approach means flexible documents can differ from one to the next within the same collection, and that difference compounds as more collections and more document variants enter the pipeline.

The part of this that catches teams off guard is how schema change actually announces itself, or rather, doesn't. A new field appearing, an existing field changing type, a nested structure getting restructured: none of these produce a DDL event the way an equivalent change in a relational database would. The change stream just starts delivering documents in the new shape, with no structured signal marking the transition. Left undetected, that drift doesn't throw an error. It produces silently incorrect or incomplete data at the destination, which is a much harder failure to catch than one that fails loudly.

What exactly-once delivery requires of pipeline design

Change streams guarantee only at-least-once delivery, so pipelines that don't deduplicate will process some events more than once. Without deduplication logic built into the pipeline, those duplicates turn into incorrect aggregations and inconsistent numbers at the destination, the kind of error that often isn't noticed until someone questions a report.

The resume token bookmark pattern provides real durability against crashes, ensuring a restart never replays events past the last batch that was durably confirmed. Persisting the token too early reintroduces exactly the data loss the pattern was meant to prevent.

The distance between "the API supports resume tokens" and "the pipeline delivers exactly once" is filled entirely by engineering work that MongoDB does not provide: explicit bookmark persistence timed correctly, upsert semantics on the sink side, and deduplication logic running somewhere in the pipeline. None of this is a flaw specific to MongoDB. It's the standard condition of CDC built on an at-least-once substrate, and any engineer who has worked through these failure modes before will recognize the shape of the problem immediately.

Three architecture patterns for moving MongoDB changes to a warehouse

Three architecture patterns cover most of how teams move MongoDB changes into a warehouse, and they differ mainly in where they place the operational burden of managing oplog consumption, buffering, schema mapping, and delivery guarantees, not in whether that burden exists. Every one of the three has to solve the same set of problems; the only variable is who solves them and where.

The first pattern reads change streams and writes directly to the warehouse, using a mechanism like Snowpipe Streaming or the Snowflake Ingest SDK. If the warehouse becomes unavailable, the pipeline has to buffer events in application memory or risk losing them, and because there's no durable intermediate queue sitting between the two systems, oplog expiry risk runs highest in this pattern of the three.

The second pattern places a durable message broker, a Kafka-compatible system, between MongoDB and the warehouse. What it costs in return is operational complexity: a broker cluster to manage, connectors to configure, a schema registry to maintain, consumer lag to watch, and a full stack of independent systems that all have to be running correctly before a single row reaches the warehouse.

AWS DMS is sometimes grouped into this conversation, and it was built to handle both one-time migrations and continuous replication. Teams evaluating DMS for continuous MongoDB replication should weigh those documented limitations against their own throughput and latency requirements before committing to it.

The third pattern is a managed CDC platform that handles oplog consumption, schema mapping, and delivery guarantees as built-in defaults. What it trades away is the granular control a team gets from assembling the first two patterns by hand, in exchange for not having to build and operate that assembly themselves.

Readiness for production use

Change streams are the right mechanism for MongoDB CDC. They are far more reliable than tailing the oplog directly and far more capable than polling a collection on a timer. None of that settles the harder question, which is whether a given pipeline built on top of the API is actually ready for production, and the gap between an API that supports something and a pipeline that reliably delivers it is where most production failures originate.

Teams that are not prepared to take on monitoring oplog lag, tuning window sizes, keeping consumers idempotent, and watching for schema drift as a continuous operational responsibility have a real choice to make: build and own that infrastructure, or adopt a managed replication service built to treat these failure modes as defaults rather than as custom engineering problems to solve from scratch. Either path can work. What matters is making that choice with a clear view of what is being taken on.

More in Change Data Capture