Node JS Event loop Craziness you may not know!

How does NodeJS work?

Bharat Raj Meriyala
6 min readApr 14, 2019

From w3schools website, Node.js is single-threaded, non-blocking & asynchronous programming model.

Non-blocking and asynchronous are good to have features any given day, but if NodeJS is single threaded aren’t we missing out on fully utilizing the capabilities of most modern processors which are multicore and multithreaded in nature?

The answer is both yes and no. Node is single threaded by design. That means when we fire up the command like this:

node server.js

The task manager on windows will show a single process called node, and inside this process we have a single thread on which our program code is running.

So, how does NodeJS become efficient this way?

Event Loop

The key to make the Entire NodeJS efficient basically is this concept of Event loop. Lot of people have tried to explain Event loop in their own way, but the fact is Event loop doesn’t run in a separate thread of its own. Event loop is like a main program(Or brain for human body) for NodeJS, executing our userland code written in server.js as part of its execution. But the key to understand is, this Event loop is single threaded.

Phases in Event loop

For the sake of understanding, we can say, as soon as we fire node command, a single thread of Event loop starts, which picks up the javascript code written in server.js and starts executing it immediately. But during our execution of Event loop detects the presence of callbacks written inside

  1. setTimeout(), setImmediate() & setInterval(). The are standard javascript functions, that help us execute code at some later point of time.
  2. OS tasks like waiting on server port for some incoming connection requests.
  3. Long running tasks like FS modules performing I/0 operations.

Now this is where the entire crux of Event loops lie. So as previously said, event loop, will first execute entire userland code, and then execute whatever code is inline and at the same time, divides all our callback functions in various phases.

During First phase, Node looks at pending timers and sees if any callback functions are ready to be called. These timer functions include the setTimeout() and setInterval().

During second phase, Node looks at pending OS tasks(like server port waiting) and other long running tasks (like FS module file I/O) and calls relevant callbacks if they are finished.

After the first 2 phases, Node moves on to third phase where it will start polling to check if, and pending OS task is completed OR long running task is completed OR a timer is about to be completed.

Once the polling finishes and node determines its time for breaking this brief pause, it moves onto execute callback functions set under setImmediate().

At the end of all 4 phases, in the fifth and final Node will continue try to call any close events. All the above 5 phases run in one single tick.

Who performs these long running tasks?

The most important phase in the above 5 steps is phase 2, when Node will look at pending OS tasks and other long running tasks. Usually these are tasks which are most time consuming and resource heavy. So how does Node with its single thread model ensure that these tasks are performed in non-blocking asynchronous fashion?

This is performed with a help of special library called libuv. libuv (Unicorn Velociraptor Library) is a multi-platform C library that provides support for asynchronous I/O based on event loops. A good understanding of libuv is critical in solving the performance behavior problems in event loops. libuv is a library which is used by NodeJS to perform all the operating system related tasks like disk I/O or long running tasks but not Network related tasks. In other words, whenever NodeJS sees any logic which requires OS resource interactions it is assigned to libuv. Libuv is multithreaded in nature and starting a instance of node with command,

node server.js

will start by default 4 threads of libuv which are ready to accept the long running requests from Node. So in simple terms even though Node is single threaded the node modules like FS, crypto modules on which it depends make use of libuv library which is multithreaded. It is also possible to increase or decrease the no of threads that libuv creates by default through a single variable like this at the top of our server.js file.

process.env.UV_THREADPOOL_SIZE = 5;

The above statement basically creates 5 threads of libuv that are waiting for requests from NodeJS.

Is this the End?

So yeah, multi threaded libuv, single high performing Event loop and NodeJS is looking easy. Well, not quite!

Not everything is handled by libuv threads in NodeJS. For some of the tasks, NodeJS will depend on Operating system async handlers as well. In such cases, it is the responsibility of Operating system to perform the threading as required and provide the results back. But the work of coordinating with Operating system async helpers is still performed with the libuv library and invoked by NodeJS. There is no change to that.

The next obvious question is what modules are dependent on OS async helpers than thread pool of libuv. We can safely assume that almost everything around networking for all Operating systems, use the OS async helpers. The Event loop would deal with these OS async helpers in the same it waits for the response from the default threads of libuv.

To take this further, I wrote a simple nodejs program, that performs a simple HTTPS request first, then calls a special Hashing function called as pbkdf2.

In the above example, we are using pbkdf2 function of crypto module, which is explicitly known to use libuv threads. For more details on my statement, you can visit the node_crypto.cc file inside the NodeJS github source code.

https://github.com/nodejs/node/blob/master/src/node_crypto.cc

We are doing a network operation and a File operation and a doHash() operation. Lets see the output of this file:

The above results state that doReques() which is purely a task that requires network is taking 5455 ms units of time for completion, unlike hash function and FS module which are directly the responsibility of libuv threads. Why is it that doRequest that does not depend on libuv threads taking a time equal to that of tasks that are dependent on libuv threads?

The reason for that is, even though when it comes to Network I/O libuv passes it on to operating system threads to return the results, the dns.lookup() operation is still performed by libuv threads. For more details see this video:

The link to the entire amazing video is as below:

https://www.youtube.com/watch?v=P9csgxBgaZ8&feature=youtu.be&t=17m28s

So, If I change the above code to this as per the suggestion:

Basically replacing the google.com with IP address of google servers and voila!!! the output changes as below:

A simple point that, doRequest() has moved onto top of the results because it is rightfully the first function to execute and complete!

--

--