Async/Await Performance Implications – Part 2

In my last post, I discussed the fact that Async/Await pushes the async function onto the event table when the executing thread hits an await call. On the face of it, that means that a program using async/await doesn’t ‘freeze up’ when an asynchronous operation occurs.

I also pointed out that while the whole program doesn’t freeze, the async function is paused while it waits on the await line.

The next question is what that last bit means from a performance standpoint. Here is an example piece of code using callbacks, and setTimeout to simulate an API call or other asynchronous event:

const timeoutFunction = () => {
    setTimeout(()=> {
        console.log("Done");
    }, 5000)

    setTimeout(()=> {
        console.log("Done");
    }, 5000)

    setTimeout(()=> {
        console.log("Done");
    }, 5000)
}
timeoutFunction();

As far as output goes, there is a 5-second pause, and then three instances of “Done” being logged out right away, one right after the other.

Contrast that with this code:

const asyncTimeout = () => {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, 5000);
    }).then(() => {
        console.log("Done");
    })
}

const testAsync = async () => {
    await asyncTimeout();
    await asyncTimeout();
    await asyncTimeout();
}

testAsync();

This second block of code results in a 5-second pause, the logging of “Done”, another 5-second pause, the logging of “Done”, another 5-second pause, and then finally the third logging of “Done”.

Much like the second block of code in my last blog post, we can see that inside of the async function, execution is paused at each await statement, which means that you have 3 separate, 5-second pauses as compared to the first code-block using callback functions, in which the three, 5-second pauses have a nearly 100% overlap.

In summary:
As most of you have no-doubt already deduced, using async/await for a single asynchronous call has no performance hit against doing that same asynchronous operation as a callback.

When doing multiple asynchronous operations in a single function, if the asynchronous calls build on each other (meaning that data from the first call is needed to make the second call), then you lose nothing performance-wise by using async/await, and likely dramatically simplify how you construct your code.

However, if you need to make multiple un-related asynchronous calls, then you’re better off not putting them all in the same async function because doing so eliminates any overlap in the wait for the asynchronous calls to report back.

Async/Await Performance Implications

It seems like every time I come across a explanation of Async/Await in JavaScript, the author cautions against using it because it ‘can slow things down because your code has to wait for the await call to finish before it can move forward’ or something to that effect.

That statement has always made me nervous. I’m at a point in my current project where it would be really convenient to use async/await, but I didn’t want to do that if it was going to result in my entire application freezing for a second or two while the await statement is resolved.

That seemed very unlikely, but I decided it was time to do some experimenting to make sure that I understood just exactly where the boundry of the performance hit started and ended.

Here is my first experiment:

const { Resolver } = require('dns').promises;
const resolver = new Resolver();

const ip = '204.79.197.212';

const resolveDNS = async (ip) => {
    try {
        let hostname = await resolver.reverse(ip);
        console.log(hostname);
    } catch (error) {
        console.log("Error: " + error);
    }
    
}

resolveDNS(ip);
console.log("Hey there.")

The code is just using the built-in NodeJS dns library to do a reverse lookup on an ip address that corresponds to hotmail.com.

If execution really froze the way some of those internet posts seemed to imply it might be doing, then the output would have been the hostname, followed by the “Hey there.” console.log.

Actual Output:
Hey there.
[ ‘a-0010.a-msedge.net’ ]

That’s very good, because it means that ‘everything’ doesn’t freeze up while waiting for the reverse dns call to resolve. Instead, it looks like once the code hits the await statement, it just shifts that function onto the event table where it waits until the dns query resolves, at which point it’s placed onto the event queue.

In the meantime though, the program is free to continue on executing unrelated pieces of code while it waits.

I next ran this experiment:

const { Resolver } = require('dns').promises;
const resolver = new Resolver();

const ip = '204.79.197.212';
const ip2 = '204.79.197.211'

const resolveDNS = async (ip, ip2) => {
    try {
        let hostname = await resolver.reverse(ip);
        console.log(hostname);
        let hostname2 = await resolver.reverse(ip2);
        console.log(hostname2); 
    } catch (error) {
        console.log("Error: " + error);
    }
    
}

for(let i=0; i<5; i++) {
    resolveDNS(ip, ip2);
}

console.log("Hey there.")

Actual Output:
Hey there.
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]

Again, it the "Hey there." statement is being run before any of the dns queries are resolved. It's interesting though that all of the '0010' queries are resolving before any of the '0009' queries show up.

That indicates to me that the entire stack isn't being shifted onto the event table when execution hits the 'await'. Rather, just the 'async' function is being shifted to the event table, which allows the for loop to continue to the next iteration.

So we load up the event table with 5 calls to resolve the first ip address. Then we log out the "Hey there." statement. Then the first ip query resolves, and the async function is moved down to the event queue and then onto the call stack.

That first lookup (a '0010') is printed, and then the code hits the second 'await' line and that function is moved back onto the event table to wait while the query on the second ip address is resolved.

By that point, the other 4 queries on the first ip address have all resolved and been shifted down to the event queue and then into the call stack in turn.

They each then get to the second 'await' statement and get shifted back onto the event table to wait for the resolution on the lookup for the second ip address.

At this point, the "Hey there." has been logged, and the 5 '0010' lookups have also been logged to the console. Shortly after that, events are registered indicating that the queries for the second ip address are coming back, and each of those functions are shifted down to the event queue and then onto the call stack so that the '0009' statements can be logged.

So, with those two code snippets we've proved that when code hits an 'await' statement, it doesn't freeze all execution, just execution of the async function containing the 'await' statement. I'm already over my word-count target for this blog post, so next week I'll expound on what all of this means and where it's safe to use async/await, and where doing so will potentially result in a performance hit.