faucet-source-mongodb-cdc
MongoDB Change Data Capture (CDC) source for the faucet-stream ecosystem. Tails a MongoDB Change Stream — at collection, database, or whole-cluster scope — and emits each per-document change event (insert, update, replace, delete, DDL) as a flat JSON CDC envelope.
Reach for it when you want to stream live mutations out of MongoDB into any faucet-stream sink — a warehouse, a queue, another database — with no polling and no missed changes. The opaque resumeToken is persisted to any faucet-core StateStore after every page, so a restarted pipeline resumes from exactly where it stopped: no duplicates, no gap. Paired with a CDC-aware sink, it is also an effectively-once delivery source.
Feature highlights
- Native Change Streams — tails MongoDB's oplog-backed change stream directly via the official
mongodbdriver. No polling loop, no last-modified timestamp column, no_changed_atbookkeeping on your documents. - Three watch scopes — one collection, one whole database, or the entire deployment (
cluster). Pick the narrowest scope that covers what you need. - Resumable by design — the server-assigned
resumeTokenof the last event in each page becomes the pipeline bookmark; on restart the stream re-opens withresumeAfter: <token>for a duplicate-free, gap-free resume. - Effectively-once delivery —
supports_exactly_once()istrue. Combined with an idempotent sink (sqlite / postgres / mysql / mssql / iceberg / bigquery) and a state store, the pipeline commits records and a monotonic commit token in one atomic unit. - Snapshot → CDC handoff — implements
capture_resume_position(), sofaucet replicatecan anchor the change stream before a bulk snapshot of the collection and stitch the two into a true mirror. - Server-side filtering — push an
operationTypeallowlist and arbitrary aggregation stages into the change stream so unwanted events never cross the wire. - Pre-image / post-image control — request the full document
beforeand/oraftereach change (MongoDB 6.0+ for pre-images), with explicitoff/when_available/required/update_lookupmodes. - Bounded memory — events are emitted in pages of
batch_size; peak memory isO(batch_size)no matter how busy the collection is. Withbatch_size: 0(single-page drain),max_staged_recordsis the OOM safety valve that caps the in-memory buffer and aborts cleanly rather than risking an OOM-kill. - Client built once — the authenticated
mongodb::Clientis constructed innew()and reused for the lifetime of the source.
Installation
# As a library:
# In the CLI (opt-in connector feature):
This connector is not in the CLI default build — enable the source-mongodb-cdc feature (or the source / full aggregates).
Prerequisites
Change Streams are only available on a replica set or a sharded cluster — they read from the oplog, which a standalone mongod does not maintain. The connector validates this at startup: if the connection URI points at a standalone instance, new() fails fast with a typed FaucetError::Source rather than silently producing no events.
For local development, run a single-node replica set:
# then, in the shell, once:
Connect with ?replicaSet=rs0 in the URI. Some additional requirements depending on the features you use:
- Pre-images (
full_document_before_change) require MongoDB 6.0+ and per-collectionchangeStreamPreAndPostImages: { enabled: true }. - The connecting user needs
findandchangeStreamprivileges on the watched namespace (the built-inreadrole covers a single database; cluster scope needs read across all databases).
Quick start
# pipeline.yaml
version: 1
source:
type: mongodb-cdc
config:
connection_uri: mongodb://user:pass@localhost:27017/?replicaSet=rs0
scope:
type: collection
database: appdb
collection: orders
full_document: update_lookup
idle_timeout: 30
sink:
type: jsonl
config:
path: ./changes.jsonl
state:
type: file
config:
Every change to appdb.orders lands as one JSON line in changes.jsonl; the resumeToken is checkpointed to ./state after each page so a re-run picks up exactly where it left off.
Output record schema
Every change event is one JSON object — a flat CDC envelope:
Field reference
| Field | Type | Description |
|---|---|---|
op |
string | Operation type: c (insert), u (update), r (replace), d (delete), ddl (drop / rename / dropDatabase / invalidate) |
ts_ms |
number | Wall-clock time of the change in Unix-epoch milliseconds (from clusterTime) |
namespace |
object | null | { "db": "…", "coll": "…" }. null for cluster-scope events that carry no namespace (e.g. a cluster-level invalidate) |
document_key |
object | null | Document identity key (typically { "_id": … }) |
before |
object | null | Pre-image of the document. Populated only when full_document_before_change is enabled and the collection has changeStreamPreAndPostImages turned on (MongoDB 6.0+). |
after |
object | null | Post-image of the document. Populated on inserts, replaces, and updates when full_document is update_lookup, when_available, or required. null on deletes. |
update_description |
object | null | Present on u events: { "updated_fields": {…}, "removed_fields": ["…"], "truncated_arrays": [{…}] }. null for all other op types. |
resume_token |
object | Opaque server-assigned token. The pipeline persists this as the page bookmark and passes it to resumeAfter on the next run. |
Operation-type mapping
MongoDB operationType |
op value |
|---|---|
insert |
c |
update |
u |
replace |
r |
delete |
d |
drop, rename, dropDatabase, invalidate |
ddl |
Configuration reference
Connection & scope
| Field | Type | Default | Description |
|---|---|---|---|
connection_uri |
string | — (required) | MongoDB connection URI. Must point at a replica set (?replicaSet=rs0) or sharded cluster. A standalone mongod URI causes a hard error at startup. Credentials in the URI are redacted from Debug output. |
scope |
enum | { type: cluster } |
Which change stream to open. See the scope variants below. |
scope variants:
# Whole deployment (default)
scope:
# One database (all collections)
scope:
# One collection
scope:
Filtering
| Field | Type | Default | Description |
|---|---|---|---|
operation_types |
string[] | [] (all) |
Server-side $match allowlist on operationType. Empty = every operation. Example: ["insert", "update", "delete"]. |
aggregation_pipeline |
object[] | [] |
Extra aggregation stages appended after the internal operation_types $match. Use for server-side field projection, redaction, or further filtering of the change-event documents. |
Document images
| Field | Type | Default | Description |
|---|---|---|---|
full_document |
enum | off |
Post-image (after) delivery: off (no full doc), when_available (include when the server can supply it), required (error if unavailable), update_lookup (re-read the current document at event time — read-skew caveat, see Caveats). |
full_document_before_change |
enum | off |
Pre-image (before) delivery: off, when_available, or required. Requires MongoDB 6.0+ and changeStreamPreAndPostImages enabled on the collection. |
Start position
| Field | Type | Default | Description |
|---|---|---|---|
start_from |
enum | { type: now } |
Where to start when no persisted bookmark exists. See the variants and precedence rules below. |
start_from variants:
start_from: # only events from now on (default)
start_from: # from the oldest retained oplog entry
start_from: # an explicit opaque token
start_from: # a cluster time, in epoch seconds
Batching & timing
| Field | Type | Default | Description |
|---|---|---|---|
idle_timeout |
seconds | 30 |
Terminate the current fetch cycle after this long with no new events. The page accumulated so far is flushed and the bookmark is saved. Must be > 0. |
max_await_time_ms |
u64 | 1000 |
Server-side maxAwaitTimeMS on getMore requests — how long the server blocks waiting for new events before returning an empty batch. Must be strictly less than idle_timeout (in milliseconds), else config validation fails. |
batch_size |
usize | 1000 |
Records per emitted StreamPage. 0 = no batching: drain until idle and emit one page. Max 1,000,000. |
max_staged_records |
usize | null | null |
OOM safety valve. Abort with a typed FaucetError::Source once more than this many change events are buffered in memory before a page is emitted. null = unbounded. Matters most with batch_size: 0 (or a library caller using fetch_all), where the buffer would otherwise grow without bound for the whole fetch cycle and risk an OOM-kill on a high-throughput stream. Mirrors the same field on the sibling postgres-cdc / mysql-cdc sources. |
Examples
Capture all changes to a collection, land in BigQuery
version: 1
source:
type: mongodb-cdc
config:
connection_uri: mongodb://faucet:secret@mongo1:27017,mongo2:27017/?replicaSet=rs0&authSource=admin
scope:
type: collection
database: myapp
collection: events
operation_types:
full_document: when_available
full_document_before_change: off
idle_timeout: 60
max_await_time_ms: 500
batch_size: 500
sink:
type: bigquery
config:
project_id: my-gcp-project
dataset_id: cdc
table_id: events_stream
credentials:
type: service_account_file
config:
state:
type: redis
config:
url: redis://localhost:6379
namespace: faucet-cdc
Cluster-wide CDC with operation filtering, printed to stdout
version: 1
source:
type: mongodb-cdc
config:
connection_uri: mongodb://faucet:secret@mongos:27017/?tls=true
scope:
type: cluster
operation_types:
idle_timeout: 30
sink:
type: stdout
config:
format: pretty
state:
type: file
config:
Server-side projection via an aggregation pipeline
Strip large fields and redact a secret before events ever leave the server:
version: 1
source:
type: mongodb-cdc
config:
connection_uri: mongodb://localhost:27017/?replicaSet=rs0
scope:
type: collection
database: appdb
collection: users
full_document: update_lookup
aggregation_pipeline:
-
state:
type: file
config:
sink:
type: jsonl
config:
path: ./users-changes.jsonl
Streaming & batching
The source overrides stream_pages to tail the change stream natively. It accumulates change events into a StreamPage and yields a page when either batch_size events have accumulated or idle_timeout elapses with no new event. The bookmark carried on each page is the resume_token of the last event in that page, so the pipeline persists durable progress every page.
- The
batch_sizeconfig field is authoritative — a pipeline-supplied hint never overrides it. batch_size: 0is the "no batching" sentinel: drain events until idle and emit a single page. Use it for low-traffic collections where you'd rather get one consolidated page per quiet period.max_await_time_msis the inner blocking-wait knob;idle_timeoutis the outer cycle terminator. The validationmax_await_time_ms < idle_timeoutguarantees the server returns control to the connector before the idle timer fires.
Resume & state
The connector is fully resumable. After each emitted page the pipeline writes the resume_token of the last event to the configured StateStore. On the next run, apply_start_bookmark restores the token and the change stream opens with resumeAfter: <token> — MongoDB delivers events from exactly that point onwards.
start_from precedence (only consulted when no persisted bookmark exists, except as noted):
resume_tokenandtimestampvariants always override a persisted bookmark — they force an explicit start position regardless of what the state store holds.nowandearliestvariants yield to a persisted bookmark: if one exists the connector resumes from it; if none exists, the variant chooses the initial position.
State keys (one per scope) so that two pipelines on different scopes never collide:
| Scope | State key |
|---|---|
| Cluster | mongodb-cdc:cluster |
Database mydb |
mongodb-cdc:db:mydb |
Collection mydb.mycoll |
mongodb-cdc:coll:mydb.mycoll |
Durability is per-page, not per-event: the bookmark is saved once per emitted page (every batch_size events). A crash mid-page replays at most batch_size events on the next run — at-least-once unless you also enable effectively-once delivery.
Effectively-once delivery
supports_exactly_once() returns true. Set delivery: exactly_once on the pipeline and the source becomes eligible for end-to-end effectively-once semantics. The pipeline assigns a monotonic, fixed-width commit token to each bookmark-carrying page and calls the sink's write_batch_idempotent, which commits the records and the token atomically; on resume the sink's last_committed_token lets the pipeline skip any page already committed.
The CLI enforces a hard gate at config-load time (caught by faucet validate before any run):
- Source must support effectively-once —
mongodb-cdcqualifies. - Sink must support idempotent writes — one of
sqlite,postgres,mysql,mssql,iceberg,bigquery. - A
state:block must be configured. - No
dlq:block (DLQ and effectively-once are mutually exclusive in this version).
version: 1
delivery: exactly_once
source:
type: mongodb-cdc
config:
connection_uri: mongodb://localhost:27017/?replicaSet=rs0
scope:
full_document: update_lookup
sink:
type: postgres
config:
connection_url: postgres://user:pass@localhost/warehouse
table: orders_cdc
state:
type: postgres
config:
connection_url: postgres://user:pass@localhost/warehouse
Note:
full_document: update_lookupre-reads the document at lookup time, not change time (see Caveats). For a strict mirror, prefer the change-event payload itself plus an idempotent sink, and pair with thecdc_unwraptransform +write_mode: upsert— see the upsert cookbook.
Snapshot → CDC handoff
This source implements capture_resume_position(), which opens a change stream against the configured scope and reads its postBatchResumeToken (present after the first server round-trip even with no events) as a resume bookmark — without consuming any changes. The faucet replicate command uses it to anchor the change stream at-or-before a bulk snapshot of the collection, so the combined snapshot + CDC result is a gap-free, duplicate-free mirror when paired with a write_mode: upsert sink.
There is no replication slot to pin — the oplog window is the retention risk. Keep it comfortably larger than the expected snapshot duration, or the captured token may roll off the oplog before CDC starts and the stream will error. See the replication cookbook for the full handoff model.
invalidate events
An invalidate event (emitted when a watched collection or database is dropped while the stream is open) is delivered as a ddl record and then terminates the stream. The connector closes the stream and returns; a fresh faucet run is needed to start a new change stream. Persisted bookmarks from before the invalidate are no longer resumable by MongoDB — use start_from: { type: now } or { type: earliest } on the next run.
Config loading
Configs load from YAML/JSON files, environment variables, or .env files. With the CLI, point faucet run at any YAML or JSON file. Programmatically, deserialize MongoCdcSourceConfig with serde from any supported format, or use the helpers in faucet_core::config (load_json, load_env, load_env_file).
Schema introspection
Print the full JSON Schema for this source's config — every field, type, default, and enum variant — with:
This is generated from the config struct via schemars, so it never drifts from the code.
Library usage
use ;
use ;
use Arc;
# async
How it works
- One client, reused.
MongoCdcSource::new()builds themongodb::Client, validates the config's fail-fast invariants, and confirms the deployment is a replica set / sharded cluster. The same client backs every fetch cycle. - Native change stream. Each fetch cycle opens (or resumes) a change stream with the configured scope,
$matchonoperation_types, any extraaggregation_pipelinestages, the chosen full-document / pre-image modes, andresumeAfter(orstartAtOperationTime) derived from the bookmark orstart_from. - Page assembly. Events are decoded into the flat CDC envelope and buffered. A page flushes on
batch_sizeevents oridle_timeoutquiet;max_await_time_mscontrols the server's blockinggetMorewait so the idle timer can fire promptly. - Bookmark tracking. The
resume_tokenof the last event in a page becomes the page bookmark, which the pipeline persists after the sink confirms the write — so a token is only durable once its events are.
Lineage dataset URI
dataset_uri() emits mongodb://<host>:<port>/<database>/<collection>, /<database>, or just the host (depending on scope), with credentials stripped — e.g. mongodb://host:27017/appdb/orders for collection scope, mongodb://host:27017/appdb for database scope, and the bare host for cluster scope.
Feature flags
This crate has no optional features of its own. From the umbrella / CLI it is gated behind:
| Feature | Enables |
|---|---|
source-mongodb-cdc |
this connector |
source |
all sources, including this one |
full |
everything |
Troubleshooting / FAQ
| Symptom | Cause & fix |
|---|---|
The $changeStream stage is only supported on replica sets or a startup FaucetError::Source about standalone |
The URI points at a standalone mongod. Change Streams need a replica set or sharded cluster. Run mongod --replSet rs0 and rs.initiate(), then add ?replicaSet=rs0 to the URI. |
Config error: max_await_time_ms must be strictly less than idle_timeout |
max_await_time_ms is in milliseconds and must be < idle_timeout (which is in seconds). E.g. idle_timeout: 30 (30 000 ms) with max_await_time_ms: 1000 is valid. |
start_from: earliest errors with an oplog/resume of change stream was not possible message |
The oldest available oplog entry has rolled past. Use { type: now } for fresh deployments, and size the oplog (and your tolerated downtime) so the window outlasts any gap between runs. |
| Resume fails after restart with a "resume point may no longer be in the oplog" error | The persisted resumeToken rolled off the oplog while the pipeline was down. Increase the oplog size, or restart with start_from: { type: now } (accepting the gap). |
before is always null despite full_document_before_change set |
Pre-images need MongoDB 6.0+ and changeStreamPreAndPostImages: { enabled: true } on the collection: db.runCommand({ collMod: "mycoll", changeStreamPreAndPostImages: { enabled: true } }). With required and pre-images off, the stream errors; with when_available you get before: null. |
after reflects a newer state than the change |
You're using full_document: update_lookup, which re-reads the document at lookup time. If the doc was further modified or deleted in between, after shows the later state (or is absent). Don't rely on it for a strict point-in-time image. |
| Pipeline exits cleanly but the source dropped a collection | An invalidate (collection/db dropped) arrives as a ddl event and terminates the stream. Re-run faucet run; the prior bookmark is no longer resumable, so set start_from. |
| Authorization errors opening the stream | The user needs find + changeStream on the namespace (cluster scope needs read across all databases). Check authSource in the URI matches where the user is defined. |
| No events appear even though documents are changing | Check operation_types isn't filtering them out, and that scope targets the right database/collection. A too-short idle_timeout ends the cycle before events arrive on a quiet collection — that's expected; the next run resumes. |
in-memory change buffer exceeded max_staged_records |
A high-throughput stream buffered more events than the configured max_staged_records cap before a page flushed — most likely with batch_size: 0 (single-page drain). Raise max_staged_records to fit your available memory, or set a non-zero batch_size so pages flush more often. |
Caveats
- Replica set or sharded cluster required. Standalone
mongodinstances do not support Change Streams; the connector validates this at startup and returns a typedFaucetError::Sourceimmediately. full_document: update_lookuphas at-least-once / read-skew semantics. The document is re-read from the primary at delivery time, not at change time. If the document was further modified or deleted in between, theafterimage reflects the later state (or is absent).start_from: earliestmay error if the oplog has rolled past the earliest timestamp. Keep the oplog window large enough for your expected downtime.- DDL events are best-effort. Collection drops/renames arrive as
ddlrecords, but the change stream does not replicate index operations orcollModchanges that don't appear in the oplog. - Per-batch durability, not per-event. A crash mid-page replays at most
batch_sizeevents (unless effectively-once delivery is enabled).
See also
- State & resumability cookbook — bookmarks, durable state stores, effectively-once.
- Replication cookbook — the snapshot → CDC handoff with
faucet replicate. - Upsert cookbook —
cdc_unwrap+write_mode: upsertfor a true CDC mirror. - Connector reference — the full capability matrix.
faucet-source-mongodb— query-mode MongoDB source (find()snapshots).faucet-state-redis/faucet-state-postgres— durableStateStorebackends for resumeToken bookmarks.faucet-source-postgres-cdc/faucet-source-mysql-cdc— CDC sources for other databases.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.