🚀 Meteor 3.5-beta: Change Streams & Performance improvements

Can you elaborate what issues you’re talking about in regard to Electron? As you know, I have modernized the meteor-desktop package completely, so would like to know if that solved the issues you have seen previously or if they still exist.

Thanks in advance

I believe you fixed it now… but man I was having a total crisis as my app, Color Schemer, would not build and I could not launch any updates. That will change soon! I will keep you posted on my progress.

1 Like

Do we think there might be any possibility to get HMR working with packages?

I love “bundling” features into packages, so many times it’s useful for “compressing” the codebase. I’m finding it even more useful in the agentic coding era because it can limit the context much better for the agent and get it to perform better.

However, the reload->rebuild on every code change is painful, especially when you are just tweaking some method calls or React components.

I’ve also been running into an issue with rspack very often, where after an edit, it just says “cannot find module … file.js”, and the only way to fix it is to run meteor reset and start the app again.

1 Like

Not in 3.5 since we are focusing in ddp/transport/reactivity fronts, maybe in 3.6 or later

Noticed this on 3.5-beta.7.

meteor npm update
npm warn Unknown env config "nodedir". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.

Also when running this shows up a few times:

(node:61590) Warning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.

[Rspack Client Error] npm warn Unknown env config "nodedir". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.

[Rspack Server Error] npm warn Unknown env config "nodedir". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.

Got it :+1:
Since npm 11.2.0, npm warns when it sees unknown environment configs (npm/cli#8153). NPM_CONFIG_NODEDIR now triggers that warning because nodedir is a node-gyp config, not an npm one. npm also warns that this will stop working in the next major version

3 Likes

HI,

For my information, when is beta.8 planned?

Yeap, right after the official release of 3.4.1. I’m not launching it in parallel so I don’t compete for testers and attention. Probably beta.8 will be the last one

2 Likes

@italojs

I upgraded WeKan to Meteor 3.5-beta.7 and Node.js 24.14.1. Does someone know how to fix rspack errors at these GitHub Actions?

Have you tried to run meteor update --npm before running the app or tests followed by meteor npm install?

You either run that as part of your GA pipeline or just ensure when you update locally you do it after upgrade Meteor version and commit any npm dependencies bump happened if any.

I’d apply it to GA anyway similarly as done for Docker envs:

(meteor update --npm 2>/dev/null || true) && meteor npm install

2 Likes

It seems there is some freezes, data loss etc.

i’ll try reproduce it and will bring news soon

[edit]
PR fix: improve ChangeStream synchronization by passing specific fence t… by italojs · Pull Request #14362 · meteor/meteor · GitHub

1 Like

How do I verify that the system is using change streams instead of oplog tailing?

const c = new Mongo.Collection('collection');

const handler = await c.find({}).observeChanges({
      added: function (id, fields) {
        // [...]
      }
    });

const observeDriver = handler?._multiplexer?._observeDriver
console.log(observeDriver)

// output
//_usesOplog: false
// _usesChangeStreams: true

cc: @minhna

2 Likes

Meteor 3.5-beta.10 is available

Beta 10 of Meteor 3.5 is the last beta (I hope :sweat_smile:) before an RC. It lands a new DDP transport architecture, makes Express endpoints first-class citizens of Meteor’s auth model, brings native MongoDB collation to both server and Minimongo, removes one of the longest-standing allocation hotspots in DDP, and fixes a subtle but painful Change Streams bug. Here are the 5 highlights you should know about.


1. Pluggable DDP transport architecture — pick the right trade-off for your app

PR #14231 (by @dupontbertrand)

Until 3.5, the DDP transport was hard-wired to SockJS. Beta 10 refactors the transport layer into a pluggable architecture: each transport is an isolated module behind a common interface, and the client uses native WebSocket whenever a non-SockJS transport is selected.

This unlocks substantial performance gains in Meteor’s real-time layer, modern backends like uws cut round-trip latency. The official benchmarks will come soon.

Available transports:

  • sockjs (default) — maximum compatibility behind strict proxies, polling fallback

  • uwsuWebSockets.js, native C++ implementation

Switch with one env var or one settings flag — no application code changes:


DDP_TRANSPORT=uws meteor run


{ "packages": { "ddp-server": { "transport": "uws" } } }

:page_with_curl: Documentation


2. Authenticated REST endpoints with accounts-express

PR #14091 (by @nachocodoner)

This closes a long-standing gap: until now, an Express route mounted on a Meteor app couldn’t see Meteor.userId() or Meteor.user() — every team rolled their own token-parsing middleware to bridge the two worlds.

Beta 10 ships a new accounts-express package with an Accounts.auth() middleware that gates routes on authentication (or any custom condition you write). Inside a protected route, the standard Meteor user context Just Works:


import express from 'express';

import { WebApp } from 'meteor/webapp';

import { Accounts } from 'meteor/accounts-base';

const app = express();

app.get('/api/me', Accounts.auth(), (req, res) => {

// Meteor.userId() and Meteor.user() are available here

res.json({ user: Meteor.user() });

});

WebApp.handlers.use(app);

On the client, a new Meteor.fetchWithAuth helper sends the auth token automatically, so consuming your own protected endpoints from a Meteor client no longer means hand-writing header logic.

This makes REST a first-class citizen of Meteor’s auth model — alongside methods and publications — without bespoke glue code.

:page_with_curl: Documentation · Meteor.fetch / Meteor.fetchWithAuth


3. MongoDB Collation Support — real case-insensitive search, end to end

PR #14188 (by @mvogttech)

If you’ve ever needed case-insensitive lookups in Meteor, you’ve probably bumped into generateCasePermutationsForString — a workaround built on top of giant $or queries that’s been around since MongoDB v2. It exists because collation options passed to Collection.find() were silently dropped by the driver, and Minimongo had no concept of locale-aware string comparison at all.

Beta 10 adds full MongoDB collation support to find, findOne, findOneAsync, and observeChanges — and implements collation-aware comparison in Minimongo using Intl.Collator, so client and server behave identically. Crucially, it’s compatible with oplog tailing: no fallback to polling.


// Case-insensitive find — same result on server and Minimongo

const users = Users.find(

{ email: 'Alice@Example.COM' },

{ collation: { locale: 'en', strength: 2 } }

).fetch();

// Locale-aware sort

Posts.find({}, {

collation: { locale: 'en', strength: 2 },

sort: { title: 1 },

}).fetch();

// Backed by a collation index

await Users.createIndexAsync(

{ email: 1 },

{ collation: { locale: 'en', strength: 2 } }

);

International, accented, and case-mixed text now behaves the way users expect — no regex tricks, no duplicated lowercase fields, no silent fallback to polling on busy collections.

:page_with_curl: Documentation


4. Zero-clone stringifyDDP — a quiet, big perf win

PR #14213 (by @mvogttech)

Every DDP message sent to every client used to be deep-cloned via EJSON.clone(msg) before serialization, purely to avoid mutating the caller’s object. With 500 clients receiving the same document change, that’s 500 deep clones producing the same string.

Beta 10 replaces this with a wire-format builder that reads the original message directly and uses copy-on-write EJSON.toJSONValue per field — only allocating for subtrees that actually contain EJSON types. It also adds a fast path for payload-free messages (ping, pong, ready, removed, nosub), and fixes a double-serialization bug in Session.send() when Meteor._printSentDDP was enabled.

Benchmark (500 clients × 1,000 iterations):

| Message | Before | After | Speedup |

|—|—|—|—|

| changed with cleared fields | 729 ms | 225 ms | 3.2× |

| changed (all cleared) | 660 ms | 163 ms | 4.1× |

| added (with Date) | 440 ms | 329 ms | 1.3× |

| added (20 fields, 1 Date) | 1164 ms | 968 ms | 1.2× |

| ping / ready | 66–118 ms | 48–84 ms | 1.3–1.4× |

No API change, no opt-in needed — just less CPU time and fewer allocations on every DDP-heavy server.


5. Fix: ObjectID fields sent as binary with projections under Change Streams

PR #14238 (by @italojs)

A bug surfaced after the Change Streams driver shipped: when using observeChanges with a field projection, ObjectID fields were arriving on the client as raw binary buffers instead of Mongo.ObjectID instances.

Root cause: the Change Streams driver receives fullDocument with native BSON types and was passing it directly to LocalCollection._compileProjection, whose internal EJSON.clone coerced ObjectId values into Binary. The oplog driver wasn’t affected because it re-fetches through Meteor’s cursor layer, which already handles type conversion.

The fix applies replaceTypes(doc, replaceMongoAtomWithMeteor) before projecting, so native BSON types become Meteor equivalents before _compileProjection runs. Regression tests cover initial add, insert, and update events.

If you tried Change Streams in earlier 3.5 betas and saw weird ObjectID values on the client when projections were involved — this is the one.


How to try it


meteor update --release 3.5-beta.10

For the full picture, including all bumped packages and other improvements (EJSON copy-on-write, async DDP rate limiter matchers, DDP session resumption, accounts-base asyncification, and more), see the 3.5 changelog.

what’s next?

Thanks

Huge thanks to @dupontbertrand, @nachocodoner, @mvogttech, @alextaaa, and everyone who tested the betas and filed feedback in the forum thread. Beta 10 is the result of that back-and-forth — keep it coming.

9 Likes

Great to see how impactful Meteor 3.5 will be for runtime performance, according to all benchmarks and changes made, not only on the Mongo handling side, but also in the general transport layer and reactive processing. :rocket: Great work @italojs, @dupontbertrand, @mvogt22, and everyone involved in this performance work!

I also want to say that Meteor 3.5-beta.10 includes the changes from the recent Meteor 3.4.1 release. So there are no more parallel releases with different contents. If you already migrated your app to Meteor 3.4.1, you can benefit from the changes in 3.5-beta.10 plus everything delivered in Meteor 3.4.1.

2 Likes

Thank you. This is mine:

observeDriver <ref *2> ChangeStreamObserveDriver {
  _usesChangeStreams: true,

I guess It’s using changeStreams now.

2 Likes

Love these DDP improvements. I assume they are backwards compatible on DDP protocol level? I am asking because we developed custom DDP protocol implementations for our Unity and VisionOS apps.

1 Like

I can speak to the PRs I contributed – the DDP contract is not broken. Same functionality, just faster / less resource usage.

5 Likes

Sounds awesome, thanks for the clarification!

1 Like