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.

Leave a Reply