Meteor 3 removed Fibers. That single change is why most Meteor 2 apps cannot simply bump a version number: every server-side call that used to block "synchronously" on a Fiber — Collection.findOne, Meteor.call inside a method, HTTP.get — now has an async counterpart, and the old forms are gone. If you maintain a production Meteor app, this migration is the gate to a supported Node.js and a maintained platform. Here is the plan we use to get through it without a code freeze.
Understand what Fibers were doing for you
Fibers let Meteor pause a server "thread" mid-function while I/O completed, so code like this worked without callbacks or await:
Meteor.methods({
invoiceTotal(orderId) {
const order = Orders.findOne(orderId); // blocked on a Fiber
const lines = Lines.find({ orderId }).fetch();
return lines.reduce((sum, l) => sum + l.amount, 0);
},
});
Under Meteor 3 the same logic must be explicit:
Meteor.methods({
async invoiceTotal(orderId) {
const order = await Orders.findOneAsync(orderId);
const lines = await Lines.find({ orderId }).fetchAsync();
return lines.reduce((sum, l) => sum + l.amount, 0);
},
});
The mechanical part — renaming findOne to findOneAsync and adding await — is the easy 80 percent. The hard 20 percent is everything that depended on Fiber semantics implicitly: call ordering, Meteor.defer blocks, code that relied on a method running to completion before the next one started, and third-party Atmosphere packages that still call the sync API internally.
Inventory before you edit
Start with a census, not a code change:
- Grep for the sync API surface.
findOne(,.fetch(),.count(),insert(,update(,remove(,upsert(,Meteor.call(on the server,HTTP.call,Assets.getText. Put the counts in a spreadsheet by directory. This number is your real scope, and it makes the estimate honest. - Inventory Atmosphere packages. Every package that touches collections or method calls on the server must be Meteor 3 compatible. Check each against its repository; for abandoned ones, decide now whether you fork, replace, or inline the two functions you actually use.
- Find the implicit-ordering bets. Search for
Meteor.defer,Meteor.setTimeout, and any method that reads its own writes. Under async execution, two method invocations from the same client can now interleave differently. Meteor 3 keeps per-client method ordering unless you opt out, but in-method assumptions ("the insert finished before I queried") needawaitto stay true.
Convert in dependency order, behind tests
We convert bottom-up: shared server utilities first, then collections helpers, then methods and publications, then startup code. Each layer lands as its own pull request, and each PR carries tests for the code it touches — this migration is the best forcing function you will ever get for adding server-side test coverage to an older app.
Two practical rules keep the diff reviewable:
- Never mix a rename and a behavior change in one commit.
findOne→await findOneAsyncshould be provably mechanical. If you spot a bug mid-conversion, note it and fix it in a separate change. - Let the linter carry the load. Enable
@typescript-eslint/no-floating-promises(or the JS equivalent) early. A missedawaitonupdateAsyncis the classic post-migration bug: the write usually still happens, but errors vanish and ordering becomes a coin flip. The linter finds these; code review mostly does not.
Meteor 2.8 through 2.16 already expose the *Async collection methods, which is the single most useful fact for planning: you can convert almost the entire codebase while still running Meteor 2 in production, shipping normally the whole time. The final switch to Meteor 3 then changes the platform, not your application code.
The last mile
When the async conversion is done on Meteor 2.16, the remaining steps are a short, careful list: upgrade to Meteor 3, replace or drop the packages that never made the jump, move any Blaze-adjacent server helpers off removed APIs, and re-run your load tests — method throughput characteristics change when Fibers stop serializing work, usually for the better, occasionally in ways that surface a hidden race.
Plan for the tail. In our experience the mechanical conversion of a mid-size app is measured in weeks, but the tail — the one abandoned package, the cron job nobody owned, the this.unblock() that was load-bearing — is where schedules slip. An upfront inventory turns those surprises into line items, which is exactly what a migration plan is for.
If your team is staring at a large Fibers-era codebase and a Node version that is aging out, this is a solvable, boundable problem. It rewards preparation and punishes improvisation — do the census first.