Change streams are now the default reactivity backend
Meteor 3.5 flips the switch: MongoDB change streams replace oplog tailing as the primary mechanism for real-time data synchronization. This is not a minor tweak. It rewires the observe driver—the core that powers every subscription, live query, and observeChanges call.
Why it matters: the oplog tax
Before 3.5, every Meteor app server tailed the database's replication log (oplog) to detect changes. This worked, but with two hidden costs:
-
You paid for writes you didn't make. Oplog is the whole database's write log. External ETL jobs, migrations, or third-party services all generated oplog entries that every Meteor server had to read and discard. Your CPU bill went up because of writes your app never even made.
-
No real-time on managed MongoDB. Tiers like Atlas Shared and Atlas Serverless don't expose the oplog. Teams on those tiers were forced into polling—repeatedly querying the database—burning CPU and money to fake instant updates.
The numbers: real-world benchmarks, not vendor hype
Community members have already posted public benchmarks running their own apps on 3.5:
- 40% more connection capacity under load (forum member @italojs). Oplog crashed at ~1,200 virtual users; change streams stayed stable up to ~1,680.
- 5x lower event loop delay (from 97ms on v3.4 to 18ms on v3.5-beta.4, reported by @mvogt22). That's an 81% reduction, visible right at the deploy line in his monitoring charts.
- 32–67% less CPU, up to 73% less RAM, 81–94% less GC pause time (community benchmark harness by @dupontbertrand). GC event counts dropped comparably. The author noted server config changed between runs, but the direction is unmistakable.
How it works: configurable driver pipeline
Meteor 3.5 selects a reactivity driver in order: ["changeStreams", "oplog", "polling"]. You control this order via settings.json:
{
"packages": {
"mongo": {
"reactivity": ["changeStreams", "oplog", "polling"],
"changeStream": {
"delay": { "error": 100, "close": 100 },
"waitUntilCaughtUpTimeoutMs": 1000
}
}
}
}
To pin the old behavior, set reactivity to ["oplog", "polling"].
Trade-offs to know
- Change streams are efficient for targeted queries. A very broad selector or highly-mutated collection pushes matching work to MongoDB, which can become the bottleneck.
- Oplog shifts load back to the app server—better when Mongo is your constraint, but it scales poorly under high global write volume.
- Polling is the expensive last resort.
Guidance: narrow your selectors, add indexes, and force oplog for specific broad-filter collections by reordering their drivers.
What else shipped in 3.5
- DDP session resumption – survives dropped connections.
- Pluggable DDP transport – includes a uws option, plus a fully functional
DISABLE_SOCKJS. - EJSON optimizations – fewer allocations on the hot path.
- accounts-express – first-class authenticated routes for APIs and auth.
- Async
DDPRateLimitermatchers and promise-based client logins. - Node.js 24 – the platform now runs on the latest Node LTS.
- Modernized dependencies – MongoDB collation, OTPAuth for 2FA, http-proxy-3.
The long road to default
Change streams were essentially done by the first beta (2026-02-11). The four and a half months to release were spent earning confidence. Twelve betas and two release candidates ground through correctness issues:
- PR#14238 fixed ObjectID fields being delivered as binary when a query used a projection.
- PR#14389 closed race conditions in
ChangeStreamObserveDriverwhere events could be dropped during snapshot/restart/watch setup, added a polling fallback for skip/limit cursors, and preserved uws listeners across restarts. - Meteor-to-native BSON and ObjectID translation had to be handled correctly at every selector and matcher boundary.
- A standalone-Mongo mis-detection could drop the driver into a change-stream restart loop.
What you should do now
- Upgrade to Meteor 3.5 (
meteor update). - Remove any oplog-specific configuration. The new defaults just work.
- Monitor your event loop delay and GC metrics. Expect significant improvements.
- If you use managed MongoDB (Atlas Shared, Serverless), test real-time subscriptions – they should now work without polling.
- For collections with very broad selectors or high mutation rates, consider pinning oplog for those specific collections by adjusting the driver order in settings.json.

