In pre-3.x versions, it’s documented and confirmed that async methods implicitly call this.unblock(), meaning multiple method calls from the same client can run concurrently.
In Meteor 3.x (which no longer uses Fibers), does this behavior still apply?
Specifically:
If I define a method like async myMethod(...), will Meteor automatically allow the next method from the same client to run concurrently?
If so, is the only way to preserve strict sequential execution to define the method as a non-async function and use Promise.await(...) inside?
I’m building a system where clients can go offline and replay queued Meteor.call() calls upon reconnect, and I need to guarantee these method calls are processed in strict order on the server side.
Yes, all methods are async and run non-blocking and if you need a sequence you can await the result and use the result into another method. You experience async in 2 main areas (but not only): DB calls and method calls.
What I find to work best with the client side (method calls):
I think others prefer try-catch and again and I think you can also await as below.
const firstResult = await Meteor.callAsync('method', {}).then(res => res) // the final return is the return of the 'await'
const secondResults = await ... // on something depending on the first result.
For DB calls, the same thing. You can ‘fire and forget’ or preserve strict sequential execution like this
'methodName': async () => {
Collection.insertAsync({...}) // will eventually do the insert, no questions asked, no assurance.
const { _id } = await Collection.insertAsync({ ... })
const result = // ... a function that dependends on _id from above will wait for _id to be resolved.
// You can also use Promise.all(). Example from Perplexity AI:
const userQuery = Meteor.users.findOneAsync({ _id: 'user123' });
const ordersQuery = Orders.find({ userId: 'user123' }).fetchAsync();
const productsQuery = Products.find({ category: 'electronics' }).fetchAsync()
// Execute all queries concurrently
const [user, orders, products] = await Promise.all([
userQuery,
ordersQuery,
productsQuery
]);
// Return specific result (user data) with references to other queries
return {
userProfile: user, // Using result from first query
orderCount: orders.length,
featuredProducts: products.slice(0, 3)
};
}
Thanks a lot for the detailed explanation, Paulishca!
Your breakdown of Meteor.callAsync, insertAsync, and the Promise.all example really helped clarify the async flow for me. I especially liked the concurrent query pattern — super useful!
No, Meteor would not automatically run the method calls concurrently
Meteor still always preserves strict sequential execution by default
By default, Meteor 3 runs methods one at a time on the server for each session unless this.unblock is called, like it did with Meteor 2. There was a bug in some 1.x and 2.x releases where Meteor would automatically unblock methods that used promises, but we made sure it was fixed before Meteor 3.
"
The message handler is passed an idempotent
// function ‘unblock’ which it may call to allow other messages to
// begin running in parallel in another fiber (for example, a method
// that wants to yield). Otherwise, it is automatically unblocked
// when it returns.
//
// Actually, we don’t have to ‘totally order’ the messages in this
// way, but it’s the easiest thing that’s correct. (unsub needs to
// be ordered against sub, methods need to be ordered against each
// other).
processMessage: function (msg_in) {
var self = this;
if (!self.inQueue) // we have been destroyed.
return;
// Respond to ping and pong messages immediately without queuing.
// If the negotiated DDP version is “pre1” which didn’t support
// pings, preserve the “pre1” behavior of responding with a “bad
// request” for the unknown messages.
//
// Fibers are needed because heartbeats use Meteor.setTimeout, which
// needs a Fiber.
"
The behavior you describe, I think, is also consistent with the server actions in NextJS (preservation of the sequence vs parallelism).
This example here (Server Actions in Next.js are executed sequentially | hmos.dev) is about something different. However, I think it shows the difference between calls in sequence from the client and “parallelism” which is basically like calling a method of methods that resolve on the server before 1 single results it being returned to the client.
I just added this.unblock() in those 2 methods in the first image and ended up with the second image which is what I actually expected.
@zodern thanks a lot for clarifying this. I wondered many times what that “wait time” was about and since I wrongly assumed unblock() was of no use, I was left with no answers.
@nachocodoner I think the documentation around unblock() is very thin and could use a more detailed and visible explanation of data types. Also the code comments in the Meteor repo could use some updates. I might have missed it in the documentation but I think unblock() could send/link to a subsection in Methods were sequence and parallelism is explained.
I thought we had moved on from these gotchas of Meteor. Nevertheless, we kept our this.unblock() calls for all our methods through our overload function, so we have control over when to await or not a client method.