DEV Community

Cover image for This is why your Node.js application is slow
Michael Owolabi
Michael Owolabi

Posted on • Edited on

This is why your Node.js application is slow

Many performance-related issues in Node.js applications have to do with how promises are implemented. Yes, you read that right. How you implemented promise in your Node.js app is most likely the culprit for how slow your app has become 🙈.

Promise is one of the popular ways of implementing asynchronous programming in Node.js which is a really good improvement over callbacks. First I’ll like us to get the literal meaning of promise outside of Node.js:

a statement that tells someone that you will definitely do or not do something
source

The keyword in the definition above is “WILL” which signifies sometime in the future. It simply means that a promise is an assurance of something that will happen in the future.

This is the exact concept of promise in Node.js which denotes that when we try to perform some operations whose results we cannot get immediately, we instead get an “assurance” of the result that will be available sometime later. The important question then is “while we wait for the promise to get fulfilled, is it ideal to “idly” wait and not execute other parts of the program, especially the ones whose results can be readily available or not?”

The answer to the question above will inform how you would work with almost inevitable promises in various parts of your applications.

There are many ways of working with promises in Node.js but async/await is a really nifty approach that many have grown to love over the years since its introduction. The truth is lots of .then in promise chaining is not very easy to keep track of when they grow to a considerable length (see example here) neither is callback (Callback hell see here). So it’s understandable why many will choose the more convenient and cleaner async/await but which sometimes can be injurious to the overall performance of your application when not applied correctly.

async/await <--> async/ablock 🤐

So, what is the problem with async/await? You ask.

The simple problem with it is that it is capable of slowing down your application greatly when not correctly used. Whenever a promise is marked with await in an async function, what you are saying is that, until the promise has resolved, the following code or code blocks in the function shouldn’t be executed which in itself is not a bad thing.

However, it becomes a problem when the code that follows can in fact be executed while waiting for the promise to get resolved because they are not dependent on the result of the resolved promise. Let’s consider the code below:

Result screenshot
Blocking await

In the code snippet above, even though the two awaits were unrelated they still block each other. The second promise did have to wait for the first one to resolve before it starts which means it will take double the time for all the promises to get resolved.

Below is a better to handle the promises such that they don’t block each other while still using your lovely await 😉

Result screenshot
Non-blocking await

Here we used await with promise.all to ensure that the two promises got executed in parallel which means instead of taking double the time as we had in the blocking example, the two promises got resolved together in ~2 seconds which was half the time of the blocking example. Now isn’t that good?

What to note here is that👇🏼

unrelated promises shouldn’t block each other

Does this mean related/dependent promises should block each other?

No! Depending on the case but most times, even dependent promises can be implemented in a way that ensures they are not blocking or the blocking gets reduced to the barest minimum for improved performance. Once again, let’s consider yet another example of this scenario:

Let’s say in an employee management system, you want to get the list of employees alongside their next of kin information.
In such a system, we first need to get the employee information and use that to find their next of kin which means we will have a dependent promise situation. Let us look at both the inefficient and a more efficient way to do this:

Below is the actual logic that determines how to work with the employee and next of kin data in the DB. This is where all the good and bad choices will matter:

Result screenshot
Await in loop implication

Here, the second asynchronous operation had to wait for the first to complete before starting which is fine, but the problem is in using await inside the loop which every asynchronous operation (getting next of kin) had to wait for the one before it 😳 This is bad. Don’t do it.

One of the cases of bad use of async/await is inside a loop.
Majority of the time you can and should avoid it,
at least if the performance of your application and the
time of your user matters to you then you should.

Now let's look at the better approach below:

Result screenshot
parallel promise execution result

Notice that in the code snippet above since the second operation is dependent on the result of the first one and there are no other synchronous operations that will be blocked, as a result, we waited until all employee records are available before starting the next operation that gets their next of kin information.

However, instead of each iteration of promise to wait on the one before it, the promises were stored and executed in parallel which saves immense execution time than the first approach, and the entire operation finished in ~2 seconds as opposed to the first blocking example that took ~6 seconds to complete execution.

Blocking the event loop 🚫

Another reason your Node.js application may be performing poorly is that you could be blocking the event loop in your code.

The Event loop is responsible for executing actual JavaScript and non-blocking I/O in Node.js

You can read more about the event loop here

We say that the event loop is blocked when it is not able to continue executing JavaScript while an operation that does not require the event loop (i.e non-JavaScript operation) is being processed e.g reading a file synchronously.

Let's consider the example below:
Assuming that in your application you need to work with countries and you have a list of countries as an external CSV file which you need to access in your code. In the code snippet below, the file reading operation blocks the event loop and ultimately affects the application’s throughput and performance because until the file reading operation completes, nothing else gets executed.

Result screenshot

Event loop blocking file reading result

Now, let’s consider a better way this can be done in a way that it doesn’t block.

Result screenshot

Event loop non-blocking file reading result

Since the actual reading of the file is an I/O operation that does not require the event loop, this operation shouldn’t block and that is what is done here as the event loop is freed up to execute other parts of the application until the result of the file reading operation becomes available.

The code snippet above uses callback which is just another method of implementing asynchronous programming in Node.js. This can be easily converted to promise so that you can use your lovely async/await for the same. One way of doing that will be to wrap the file reading operation in a promise and make the returned value a promise.

There are definitely more reasons why your Node applications may perform poorly in terms of performance but these are the more common ones I have seen. You are welcome to share more insights in the comment section.

Conclusion

The key things to remember regardless of which approach you chose to use when working with promises in Node.js is to ensure:

  • Unrelated promises do not block each other.
  • Non dependent promises are executed in parallel and not sequentially.
  • Don't use await inside a loop.

Regarding the event loop:

  • Whatever you do, ensure that the event loop isn’t blocked.

If you can keep these in mind, you will be intentional about taking better decisions on which approach to use so that the performance of your application doesn’t suffer.

Further Reading:

This article is majorly focused on a single approach to working with promises and its implications.
There are other ways/things to consider to achieve the same or sometimes better result when working with promises in Node.js which I encourage you to read up on in the links below:
Broken Promises - James Snell

Don't block the event loop - A Node.js guide on never blocking the event loop.

N:B
If you know of other ways of making asynchronous programming a bliss in Node.js, please do share in the comment section.

Top comments (26)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

The simple problem with it is that it blocks the event loop when not correctly used.

How does this block the event loop though? Just because one thread of execution is waiting for some data (or just a timeout) doesn't mean the rest of the application can't still be doing stuff.

What you're describing is simply a sequential fetching of data that will slow down a single thread of execution, but not the rest of the application.

Collapse
 
imichaelowolabi profile image
Michael Owolabi

Thank you @darkwiiplayer for your comment. Yes, I agree that there's a way to use async/await to ensure the other part of the program gets executed while the result of the async operation is being processed however, there's also a way as shown in the article where the event loop is blocked and no other part of the program will get executed until the result of the asynchronous operation becomes available as a result of using it wrongly.

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

Again, that's not what you show in the article. You're not blocking the event loop, you're just waiting for a timer twice in a row.

Thread Thread
 
dununubatman profile image
Joshua Dale

DarkWiiPlayer is correct. In your first example you changed the behavior so your promises are effectively running in parallel instead of running sequentially in your asynchronous example. The event loop isn’t being blocked, you just told it to wait for the first promise to complete before executing the second.

Thread Thread
 
imichaelowolabi profile image
Michael Owolabi • Edited

Thank you so much @darkwiiplayer for your follow up response and you @dununubatman for your further explanation. Yes, you're correct and I agree with you that I did not show that in the article and I am going to update that part in the article as pointed out.
I really appreciate 🙏🏻

Collapse
 
ats1999 profile image
Rahul kumar

no other part of the program will get executed until the result

This is not correct await will not block execution of the whole program. It'll pause the execution of the function.

Collapse
 
sannajammeh profile image
Sanna Jammeh

The event loop is NOT blocked by any async function unless you’re executing blocking code. Await is not blocking code.

Collapse
 
leomartindev profile image
Léo Martin

Yes, I think there is a HUGE confusion with how the event loop works.
This article is misleading.

Collapse
 
vectormike40 profile image
Victor Jonah

Great article! But I’m still confused but I think you meant to say ‘slow down the event loop’ and not block it because this long response times can not block the event loop. Things that block the event loop are bad recursion(no termination condition) and sync operations like reading a file(this blocks for a while).

I totally get your tips!

Collapse
 
imichaelowolabi profile image
Michael Owolabi

Thank you @vectormike40 for your comment. Yes, while all application that block the event loop will have slow response time the converse is not always true and that is where the confusion is here. It is not sufficiently shown in the article where or how the event loop is blocked and I'm going to update that part of the article. Thanks once again

Collapse
 
olasunkanmi profile image
Oyinlola Olasunkanmi

It is a thumb rule not to use async await in a for loop. Also a try catch block should be used alongside async await so as to catch any error that may occur.

Collapse
 
bias profile image
Tobias Nickel

I disagree of putting try catch around all await. most of the time when there is an error it should be logged, the current operation should stop and the frontend should get a error response. And this is done by frameworks nd the frameworks can often be oconfigured to do standard handling.

Only when actually doing real handling of an error like retry, or try
an alternative solution a try/catch should be used.

otherwise try catch blocks all over the place make code difficult to read (bloated) and often lead to inconsitencies how errors are logged, or discarded, responses are generated. Because we develop programs in teams not only on our own,...

Collapse
 
olasunkanmi profile image
Oyinlola Olasunkanmi • Edited

when you use a try catch block within async and await, you can catch the exception and log it.

Collapse
 
necmettin profile image
Necmettin Begiter

How did you manage to disagree with something that said the exact same thing you said?

Collapse
 
necmettin profile image
Necmettin Begiter • Edited

There are fundamental and much more basic problems with the data structure.

First of all, unless you have multiple nextofkin for your employees (which you don't), you must not keep employeeid in nextofkin data, instead, you must keep nextofkinid in employee data. This means one less index for nextofkin data, and having a reference to the nextofkin by the time you read employee id.

Second, if you have a list of employees you have already read from one table, and multiple nextofkin you need to read from another table, you should never ever read them one by one. In your data structure, the correct way is to create a list of employee ids, and fetch them all at once from the nextofkin list all at once.

Also, as far as I can see, most programmers confuse blocking the event loop with blocking the current response.

If the current response needs to read data from a database, that is not a blocking operation. The event loop will work on other things while ASYNChronously AWAITing for the data. Again, while that current response is waiting for the data, NodeJS will keep working on other requests and responses.

Collapse
 
christiankozalla profile image
Christian Kozalla

This article got me started on the event loop in Node, so I read up on it in the official documentation (nodejs.org/en/docs/guides/blocking...)

I'd like to quote an example:

"As an example, let's consider a case where each request to a web server takes 50ms to complete and 45ms of that 50ms is database I/O that can be done asynchronously. Choosing non-blocking asynchronous operations frees up that 45ms per request to handle other requests. This is a significant difference in capacity just by choosing to use non-blocking methods instead of blocking methods.
The event loop is different than models in many other languages where additional threads may be created to handle concurrent work."

Collapse
 
iliafaramarzpour profile image
ilia faramarzpour

Interesting article, I am waiting for more interesting articles from you. 😉🌹

Collapse
 
imichaelowolabi profile image
Michael Owolabi

Thank you @iliafaramarzpour glad you found it interesting

Collapse
 
bias profile image
Tobias Nickel • Edited

The changes you make are good, they cause a single API call to be faster, but it will not help to get more request per minute.

only when you use a transaction and block for the duration of the request/task any other resources, that subsequent requests need to await for to free up.

also, when you are using mysql for example, there is not going to be any difference. because the db driver (on each connection) send one query, wait the result, then send the next query. The postgres module (not the pg can send all queries instantly, and profit from the code style in this article.

Collapse
 
bias profile image
Tobias Nickel

github.com/porsager/postgres/issue...

we had some discussion about the topic in this issue.

tldr: technically it is pissible to do concurrent/pipelined transactions on a single transaction, however errorhandling can cause unwanted behavior.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
imichaelowolabi profile image
Michael Owolabi

Thank you @romeerez for your concern for good content which I agree with you that we should hold ouselves to a better standard. However, your opening statement just isn't true because the article has since been updated before your comment.

The article isn't describing n+1 problem as that isn't peculiar to Node.js.

Anyway, thank you for your concern.

Collapse
 
romeerez profile image
Roman K

As for N+1, it's the way how you're loading nextOfKin with Promise.all, it's not a proper solution for a problem.

In the example you're using locally defined data and setTimeout to simulate delay, in real world it will most likely be a database or API call.

In case of API call, Promise.all would be ok only if you have no other choice, and in case of database call this Promise.all will consume whole connection pool and block other users from accessing database, Necmettin Begiter and Tobias Nickel mentioned this in comments.

Like, if you awaiting 100 promises, and have just 10 available connections, it won't be fast and other users won't be able to make a query in the meantime.

So I was concerned that new comers may take this article as a teaching material and will go and implement it this way on real projects. But since you're editing it and taking care of it - that's good, wish you success

Collapse
 
romeerez profile image
Roman K

Sorry! My bad, really, I read it before and yesterday just very briefly looked through and I see the same problem, I see section about event loop so I thought it wasn't changed

Collapse
 
cesarqueb profile image
cesar-queb

Thank you for this useful and great article!

Collapse
 
caohoangnam profile image
J.O.E

Thanks for sharing that helpful with me