Meteor 3.5 is a broad release, and it earns the word. Its center of gravity is the reactive data layer, but the work reaches from the database driver out to your auth middleware and the Node runtime underneath. Here is the map, and everything below is covered in detail further down.
The headline is change streams by default: reactivity now works on managed and serverless MongoDB (Atlas Shared, Atlas Serverless) where the oplog was never available, so each instance does less work per subscription, your fleet gets smaller, and so does the bill. Around that sits the rest of the real-time modernization, with DDP session resumption that survives a dropped connection, a pluggable DDP transport with a uws option, a fully functional DISABLE_SOCKJS, and EJSON optimizations that trim allocations on the hot path. If you build APIs and auth, there is accounts-express for first-class authenticated routes, async DDPRateLimiter matchers, and client logins that are fully promise-based end to end. Underneath it all, the platform moves to Node.js 24, with a handful of dependencies (MongoDB collation, OTPAuth for 2FA, http-proxy-3) quietly modernized so you don’t have to think about them.
Change streams are where it starts.
The problem we kept running into
Meteor’s real-time updates rode on Mongo’s oplog: to know when data changed, every app server tailed the database’s replication log (a running record of every write to every collection) and filtered it, in your Node process, down to just the queries it cared about. It worked, but it carried two costs.
The first is that the oplog is the whole database’s write log, not just yours. Meteor tails all of it and throws away what it doesn’t need. The moment something outside Meteor wrote to the same database (a nightly ETL job, a data migration, a third-party service injecting records), every app server still had to read and discard all of that traffic. Your Meteor CPU bill went up because of writes your app never even made.
The second is that it tied you to self-managed Mongo. On managed and serverless tiers like Atlas Shared, the oplog is off-limits. So the moment you moved to the hosting most teams actually want, real-time broke, and your only fallback was polling: asking the database the same questions over and over, burning CPU and money to fake what should be instant.
Meteor 3.5 changes that. Change streams are now the default, real-time works on managed and serverless Mongo, and there’s no config to write.
And real apps are already reporting the difference, in public, with numbers.
Limitation
There is one requirement, and Meteor handles it for you: change streams need MongoDB 6+ running as a replica set or a sharded cluster. On older or standalone MongoDB, Meteor detects that it can’t use them and automatically falls back (to oplog, and then to polling). The same fallback covers cursors that use skip or limit and selectors change streams cannot serve. So the default is safe: you get the fast path where it works and a working path everywhere else, with no code changes and nothing to configure.
If you ever need the previous behavior, you can pin the pre-3.5 path for the whole app by setting packages.mongo.reactivity to ["oplog", "polling"] in settings.json (more on that, and the rest of the driver’s tuning, below).
For the full picture, including how the observer driver selects and switches strategies, see the change streams observer driver docs. The feature landed in PR#13787.
The proof: real-world numbers
We could tell you change streams are faster. Instead, here is what the community measured. These are not vendor benchmarks run on hand-picked hardware to make a launch look good. They come from Meteor developers testing their own apps and load harnesses, and posting the results in public. Read them that way.
More connection capacity under load. In the official Meteor 3.5 announcement, forum member @italojs shared a load test comparing the two reactivity backends on the same app. Change streams sustained roughly 40% higher connection capacity than oplog. Under the same ramp, oplog hit out-of-memory crashes around 1,200 virtual users, while change streams stayed stable up to about 1,680 virtual users, degrading only to timeouts rather than crashing. The post includes a montiAPM chart of both runs; see the charts in the thread for the full picture.

A real app of the most striking reports came from a production user. In the same thread, @mvogt22 (Michael) reported that after switching his app to change streams, average Event Loop Delay dropped from 97ms on v3.4 to 18ms on v3.5-beta.4, roughly a 5x (about 81%) reduction (post 32, post 37). Event Loop Delay is a direct measure of how starved your Node process is; cutting it that far means far more headroom before an instance falls over. He also posted monitoring screenshots marking the v3.5-beta.4 deploy, with the improvement visible right at the deploy line.

Directional, but pointing the right way. @dupontbertrand built a community benchmark harness (a Compare Runs dashboard that pits release-3.4.1 against release-3.5 across several scenarios), documented in the Meteor performance: a to-do list thread. The resource picture is striking. Across the ddp-reactive-light, fanout-light, and reactive-crud scenarios, 3.5 cut average app-server CPU by roughly 32–67%, average RAM by up to about 73%, and total garbage-collector pause time by 81–94%, with GC event counts down a comparable amount. Treat these as directional rather than final (the author noted that server configuration changed between runs, and wall-clock time was mixed across scenarios), but the direction is unmistakable: with change streams, the app server simply does far less work.

Numbers like these depend on running change streams well, so it is worth understanding the driver you are now running by default.
Change streams, getting stated
Change streams are the default, but the whole driver pipeline is configurable. Meteor picks a reactivity driver in order, and you control that order under packages.mongo.reactivity in settings.json. The default is effectively ["changeStreams", "oplog", "polling"]: try change streams first, fall back to oplog, then polling. To pin the pre-3.5 behavior, set it to ["oplog", "polling"].
A few optional knobs live under packages.mongo.changeStream. delay.error (default 100 ms) is how long the driver waits before restarting a stream after an error; delay.close (default 100 ms) is the equivalent delay after a clean close. waitUntilCaughtUpTimeoutMs (default 1000 ms) is the upper bound Meteor waits for the change stream to catch up when coordinating with DDP write fences. That timeout is a genuine trade-off: if it elapses, the write fence proceeds anyway, so a client can briefly miss its own write (read-your-writes). Raise it for stricter consistency, lower it to keep methods snappy under load.
{
"packages": {
"mongo": {
"reactivity": ["changeStreams", "oplog", "polling"],
"changeStream": {
"delay": { "error": 100, "close": 100 },
"waitUntilCaughtUpTimeoutMs": 1000
}
}
}
}Full details are in the change streams observer driver docs.
Trade off
Now the trade-off worth knowing: change streams are not free everywhere. They are extremely efficient for targeted queries, but a very broad selector or a highly-mutated collection pushes matching work onto the Mongo cluster, which can become the bottleneck. Oplog shifts that load back to the app server, better when Mongo is your constraint, but it scales poorly under very high global write volume and needs oplog permissions. Polling is the expensive last resort.
So, what else?
Change streams get the headline, but they are not the only reason 3.5 is faster. The whole real-time layer was reworked this release (how sessions survive a dropped connection, how DDP moves bytes over the wire, and how EJSON serializes them), and each piece contributes its own share of the speedup.
DDP Session Resumption
Sessions survive the network, not just the request. With DDP Session Resumption (PR#14051), a client that drops and reconnects within a grace period (default 15s) picks up its existing session instead of tearing everything down. Pending method calls are not lost, and active subscriptions resume where they left off. The payoff is felt on mobile handoffs, sleeping tabs, and flaky Wi-Fi: smoother UX for users, and far less server CPU during reconnect storms because you are not re-running every subscription from scratch. The grace period and message-queue limit are tunable per app.
DDP is now pluggable
Pick your transport. DDP is now pluggable (PR#14231). Keep sockjs, the default, for maximum compatibility behind strict proxies, or switch to uws (uWebSockets) for lower latency and higher throughput on internal or controlled deployments. You flip it via an env var or a settings flag, with no app code changes. See the DDP transport docs.
Drop SockJS entirely
Drop SockJS entirely when you don’t need it. DISABLE_SOCKJS is now fully functional (PR#14206). Where you don’t rely on the polling fallback, you get a smaller client bundle and fewer handshakes.
EJSON optimizations
Fewer allocations on every message. A batch of EJSON optimizations trims the cost of serialization on the hot path: zero-clone stringifyDDP (PR#14213), copy-on-write toJSONValue/fromJSONValue (PR#14209), fast-path primitive comparisons and early bail-out in EJSON.equals (PR#14208, PR#14205), and no array allocation in lengthOf (PR#14204). The net effect on a busy server: fewer allocations in DDP serialization, and so less GC pressure.
The new accounts-express package
If you have ever bolted a REST endpoint onto a Meteor app, you know the drill: pull the token off the Authorization header yourself, hash it, look up the user in services.resume.loginTokens, check expiry, and hope you got it right. Every protected route reinvented that plumbing, and none of it talked to the rest of Meteor’s account system.
The new accounts-express package (PR#14091), shipped out of the box by Nacho Codoñer, makes authenticated routes first-class. createAuthMiddleware resolves the login token from an Authorization: Bearer header or the meteor_login_token cookie, validates it, and sets up the invocation context so that inside your handler Meteor.userId() and Meteor.userAsync() work exactly like they do in a method.
import express from "express";
import { WebApp } from "meteor/webapp";
import { createAuthMiddleware } from "meteor/accounts-express";
const app = express();
// required: true -> 401 on missing/invalid/expired token
app.use("/api", createAuthMiddleware({ required: true }));
app.get("/api/me", async (req, res) => {
const user = await Meteor.userAsync();
res.json({ userId: Meteor.userId(), email: user?.emails?.[0]?.address });
});
WebApp.handlers.use(app);DDPRateLimiter
Two more wins for this crowd. DDPRateLimiter rule matchers can now be async (PR#14182), so you can gate methods and subscriptions on data-driven conditions, role, billing tier, or a feature flag pulled straight from the database. And client logins are fully promise-based: Meteor.loginWithPasswordAsync and Meteor.loginWithTokenAsync (PR#14070) round out the asyncified client accounts (PR#14069), so await works end to end.
Node 24 and the invisible work
The payoff is a single line in your terminal: run meteor update --release 3.5 and your app is on Node.js 24.15.0 and NPM 11.12.1 (PR#14176, PR#14399). No drama on your end. That was the goal, and it took more than a version number to hit it.
This was not a trivial bump. Node 24 surfaced OS-level compatibility issues with parts of Meteor’s own build and release infrastructure, forcing the team to update it before the new runtime could ship cleanly. Most of that happened below the waterline, and it is deliberately not detailed here. The point is that the friction landed on the maintainers, not on you, so that meteor update just works on Node 24.
While the platform moved forward, a few dependencies got quietly modernized too:
- Two-factor auth now runs on
OTPAuthinstead of the unmaintainednode-2fainaccounts-2fa(PR#14321), a healthier foundation with no change to your code. - The dev proxy moved from
http-proxyto the maintainedhttp-proxy-3(PR#13916). - MongoDB Collation support (PR#14188) brings reliable case-insensitive search and locale-aware sorting, consistent across client and server, without silently falling back to polling.
None of these will change how you write an app. That is exactly the idea: the platform stays current so you do not have to think about it.
Where 3.5 fits in the Meteor 3 story
Meteor 3.0 (July 2024) was about survival: tearing out Fibers and moving the entire framework to async/await, the deepest migration in Meteor history. 3.1 (November 2024) stabilized that new foundation: Node 22, the v6 MongoDB driver, Express 5, roles folded into core.
What came next is easy to miss as a pattern, but it is the whole point. From 3.2 on, every release chipped away at performance. 3.2 (March 2025) shipped the meteor profile command and fixed a long-standing oplog data-loss bug. 3.3 (June 2025) went after build and dev speed: the SWC transpiler and minifier, parcel/watcher, a modern-by-default architecture. 3.4 (January 2026) modernized the build stack itself with the Rspack integration and tools-core, cutting both bundle sizes and build times.
Notice where all of that work lived: in the build and the tooling. Faster rebuilds, smaller bundles, better profiling. The one place it had not reached was the part of Meteor that runs in production around the clock, the reactive data layer, and specifically how Meteor watches MongoDB for changes.
That is what 3.5 is. Change streams move the performance story from build time to runtime. It is the same multi-release obsession, now made measurable and public through the meteor/performance benchmark suite and the community performance to-do list, finally aimed at the single most expensive thing a real-time app does.
3.5 is not a new direction. It is the moment the performance thread that started with meteor profile in 3.2 reaches the core.
A note on how 3.5 got built
Over the past year, the number of pull requests landing on Meteor has climbed sharply. A large part of that surge is LLM-assisted contribution: people who might once have filed an issue and waited now arrive with a working patch. The barrier between “I noticed something” and “here is a fix” is lower than it has ever been, and it shows. More contributors, more ideas in flight, more of the codebase being touched by more hands.
That is a genuinely good problem to have. A livelier community is the whole point, and the throughput of ideas 3.5 absorbed would have been hard to imagine a couple of years ago.
Throughput on one side of the table is capacity on the other. Every incoming change still has to be read, understood, tested against edge cases, and reconciled with everything else in flight, real work, gladly done. This is not a question of contribution quality; it is a shared resource called review bandwidth, and 3.5 invested heavily in it while re-wiring the heart of Meteor’s reactivity.
The right response is not to slow contribution down; it is to make correctness cheaper to prove. That is why 3.5 leans harder than any release before it on measurable performance, and why the community now has a public benchmark suite to point real numbers at real claims.
That is one answer, aimed at the code. The other answer, aimed at the organization around the code, has been playing out in the open in PR#14426, a proposed GOVERNANCE.md that starts from exactly the surge this section describes: when AI makes a diff nearly free, a contribution’s value stops being its line count and becomes judgment, ownership, and follow-through. As Antonio Soto puts it in “Open Source in the AI Age”, “AI made PRs, issues, and security reports cheap. Maintainer trust, review time, and ownership stayed expensive.” The proposal, the first governance document Meteor has ever had, tries to make that expensive part legible: named areas of specialization with a clear owner for review (Accounts, DDP, MongoDB, Reactivity, the build system), and explicit credit for the non-code work (reviewing, triaging, answering in the forum, writing docs) that holds a project together.
Upgrade, what is next, and thanks
Ready to try it? Upgrade with a single command:
meteor update --release 3.5Change streams are on by default, so there is nothing else to configure. One requirement to keep in mind: change streams need MongoDB 6+ running as a replica set or a sharded cluster. If you are on older or standalone Mongo, Meteor automatically falls back to oplog or polling, so meteor update stays safe either way.
Prefer the legacy behavior for now? Set packages.mongo.reactivity to ["oplog", "polling"] in your settings.json and you are back to how 3.4 worked.
What is next. The work is already underway. A 3.5.1 patch release is in progress (it sharpens the change-stream driver with safer projection fallbacks and opt-in journal-commit tuning, moves to Node 24.16, and clears a batch of fixes), and a 3.5.2 will likely follow to keep the polish coming. Further out, Meteor 3.6 is just getting started, and it points where the roadmap does: built-in OpenTelemetry and observability, a modern successor to Cordova for mobile, faster release CI/CD, and more Rspack and TypeScript polish. The performance thread does not stop either. If you have a workload to share, the community benchmark effort is the place.
Thanks. Meteor 3.5 is the product of many hands. Our deep gratitude to @italojs, @nachocodoner, @Grubba27, @radekmie, @9Morello, @alextaaa, @dupontbertrand, @Eshaan-byte, @harryadel, @mvogttech, @OleksandrChekhovskyi, @StorytellerCZ, and @vlasky. Real-time on managed Mongo (long asked for, finally the default) is yours because of them.