+1 (478) 654-9062

After the Migration: What Meteor 3.5 Changes Under a Running App

Most of our Meteor 3 writing so far has been about getting to Meteor 3 — the Fibers conversion, the package census, the Node and MongoDB version matrix. This one is about what happens after. The 3.x line has kept moving: 3.3 brought SWC, 3.4 added the Rspack build integration, and 3.5 (June 2026) changed how reactivity is wired underneath every publication you own, with follow-up fixes through 3.5.1 and 3.5.3. If your app is sitting on 3.1 or 3.2 because "we already did the big migration," there is a second, much smaller upgrade worth planning — and a few things in it that deserve a staging soak rather than a Friday deploy.

Here is what actually changes, in the order it will affect you.

Change streams become the default observer driver

In 3.5, MongoDB change streams are the default driver behind observeChanges, replacing oplog tailing for apps that qualify. Three facts matter operationally:

  • Change streams now require MongoDB 6+ with a replica set or sharded cluster. The earlier floor was 3.6. If your cluster is older, nothing breaks — Meteor falls back to oplog tailing, or to polling if oplog access is unavailable.
  • It is on by default. Most apps get it on upgrade with no code change. There is a documented way to revert to the previous driver, and you should know what that flag is before you deploy, not while you are reading logs at 11pm.
  • Oplog access stops being a prerequisite for reactivity. Managed tiers that never exposed the oplog — Atlas shared and serverless among them — can now do real-time properly instead of being forced into polling.

That last point is the one that changes decisions rather than just configuration. "We have to stay on a dedicated cluster because Meteor needs oplog access" has been a load-bearing constraint in a lot of hosting conversations. On 3.5 with MongoDB 6+, it isn't one anymore.

The throughput claim from the release notes — more concurrent users, subscriptions, and method calls per instance — is plausible and matches the mechanism, but treat it as a hypothesis about your app until you have numbers. Oplog tailing is one process-wide reader filtered against every observer; change streams are server-side cursors scoped per watch. Which of those is cheaper depends entirely on the shape of your publications. An app with a few hundred narrow subscriptions and an app with ten thousand overlapping ones will not get the same result, and some of the load moves from your app servers toward your database. Capture a baseline first: method latency percentiles, observer counts, app-server CPU, and database CPU. Then compare.

Read the 3.5.1 change-stream fixes as a checklist

The 3.5.1 notes are unusually useful as a risk list, because each fix names a real failure mode somebody hit in production. Worth checking your codebase against:

  • A method that writes a collection and then triggers new observers on that same collection in the same call could hang forever. Fixed in 3.5.1. Login flows were a reported case.
  • Multiple MongoDB connections sharing a collection name could produce methods hanging with "change stream catching up took too long." If you use a second MongoInternals.RemoteCollectionDriver for an analytics or legacy database, this is you.
  • Nested-object projections in subscriptions crashed under change streams; they now fall back to oplog/polling as before. Grep your publications for projections like { 'profile.settings': 1 }.
  • Change-stream history loss (the server reports the resume point is gone) could put the app into a restart loop; 3.5.1 recovers and re-syncs missed events.
  • TypeScript type resolution for the mongo package was fixed so zodern:types picks up the bundled MongoDB v6 driver types instead of stale @types/meteor. If your editor has been lying to you about cursor types, that is why.

None of these are reasons to avoid 3.5. They are reasons to land on 3.5.3 or later rather than 3.5.0, and to spend a week in staging with your real publication mix before production. Patch releases exist because the first users found the edges; you do not have to be one of them.

DDP session resumption changes reconnect behavior

3.5 also added DDP session resumption: short disconnects no longer tear down and rebuild the whole session. Pending method calls survive, active subscriptions pick up where they left off, and the client stops flickering through a full re-subscribe on every mobile handoff or sleeping tab.

This is a genuine user-visible improvement on bad networks, and it reduces the reconnect-storm CPU spike that follows a deploy or a load-balancer event. It also quietly changes semantics you may have built on:

  • Presence and "online now" features. If you track connected users via onDisconnect-style logic, a brief blip no longer registers as a disconnect inside the grace period. That is usually what you wanted. Check that it is.
  • Grace period and queue limits are tunable. They trade responsiveness against server memory. The defaults are sensible; if you have a large fleet with long-lived sessions, look at the message-queue limit before you look at anything else.
  • Session-scoped server state that assumed teardown on disconnect now lives longer. Audit anything you cached per connection.

3.5 further lets you choose the DDP transport — sockjs by default for maximum compatibility behind strict proxies, or uws for lower latency on controlled deployments — switchable by environment variable with no application code change. Easy to A/B, easy to roll back. Leave it on sockjs for public traffic unless you have measured a reason not to.

Two smaller things that remove custom code

  • accounts-express ships in 3.5, giving authenticated REST endpoints that can read Meteor.userId() and Meteor.user() the same way methods and publications do, via an Accounts.auth() middleware. If you have hand-rolled token parsing in a WebApp.connectHandlers route — and most inherited apps have exactly that, written once in 2017 — this is a chance to delete it in favor of something maintained.
  • MongoDB collation support means case-insensitive search and locale-aware sorting without duplicated lowercase fields or regex tricks, with Minimongo matching server behavior. If you carry a nameLower field purely for sorting, it may now be dead weight.

Neither is urgent. Both are the kind of cleanup that pays for itself the next time someone unfamiliar opens the file.

A sane upgrade sequence

For an app already on Meteor 3.1 or 3.2:

  1. Baseline first. Method latency percentiles, observer/subscription counts, app-server and database CPU, reconnect rate. Without this you cannot tell whether 3.5 helped.
  2. Check the MongoDB floor. Change streams want MongoDB 6+ on a replica set. If you are on 5.0, that database upgrade is the real prerequisite, and it is worth doing on its own merits regardless.
  3. Upgrade in staging to the current patch — 3.5.3 or later. Run your full publication mix, not a smoke test.
  4. Walk the 3.5.1 fix list against your code. Second Mongo connections, nested projections, methods that write and then observe the same collection.
  5. Deploy with the revert path written down. Know how to switch the observer driver back and who is allowed to decide that.
  6. Re-measure, then act. If the per-instance throughput genuinely improved, your fleet size is now a question worth asking. That is a real cost conversation, with evidence behind it.

Whether this is worth your time

If your Meteor app is stable and lightly used, 3.5 is housekeeping — take it on your normal patch cadence, mainly for the security fixes in 3.5.3 and the general value of staying current on a line that is still receiving them.

If you are running a busy app with a lot of subscriptions, or paying for a dedicated MongoDB cluster chiefly because oplog access was mandatory, or fielding complaints about UI flicker on mobile networks, then 3.5 addresses those three things directly and is worth scheduling deliberately.

And if you are still on Meteor 2 reading this: none of it applies yet, and none of it changes the shape of the work in front of you. Do the census, do the async conversion on 2.16, then land on a current 3.x rather than the version that was newest when you started. The platform moved while you were migrating — that is an argument for finishing, not for hurrying.