Node.js Event Loop

Niharika Rastogi
Software Incubator
Published in
5 min readFeb 9, 2022

“Loop goes on and on and on and on and on”

Suppose you are driving on a one-lane road with tons of cars all lined up. As long as all the cars are moving with an appreciable speed there will be no problem at all. But what if a car needs to halt on the road for a few seconds? Every car behind that car will have to wait and this will cause inconvenience for everyone.

How is this related to the node.js event loop? Let’s find out.

Node.js

Node.js is an open-source and cross-platform JavaScript runtime environment.

A Node.js application is single threaded which means that there is just one thing happening at a time just like the one-lane road in the example above. So if you have tons of clients requesting your server then each of them will have to wait for their turn because the server is serving one client at a time using a single thread.

Non-blocking I/O

Now suppose a client requested a file read task for a huge file which is going to take too long and due to single threading the event loop will have to wait for this process to finish before executing any other JavaScript code. This means that any further execution is blocked until the file read operation is completed just like the whole traffic is blocked if a car needs to halt for a few seconds.

But, this is not what we observe while working with Node. It is asynchronous and can perform non-blocking I/O operations, like reading from the network, accessing a database or the filesystem. It is like, if one car wants to halt then it is raised above in the air and asked to wait there while all the other cars can continue to move.

Event loop

The event loop is an endless loop, which waits for tasks, executes them and then sleeps until it receives more tasks.

It is initialized by node when the application starts. It is going to manage all the requests that the server receives.

The JavaScript code in node.js is executed by the V8 engine which is written in C++. C++ has a special library module called libuv which is used to perform asynchronous operations and manage a special thread pool called the libuv thread pool.

Let’s dive deeper into the working of the event loop with a series of steps.

A client makes a request to the server. The event loop will check if the request is synchronous or asynchronous.

  1. Synchronous request: The C++ code, under the hood, will execute any synchronous request then and there and return the response to the client.
  2. Asynchronous request: If any asynchronous request is made then the event loop will check if there is any C++ primitive available to complete the task with the help of C++ libuv library.
  3. If any C++ primitive is available then the event loop will assign the task to that primitive and the C++ code will complete the task in the main thread itself.
    Else the task is assigned to background threads in the thread pool. This thread pool is composed of four threads which are used to delegate operations that are too heavy for the event loop.
  4. The event loop offloads operations to the system kernel whenever possible. When one of these operations completes, the kernel responds back to Node.js so that the appropriate callback may be added to the poll queue to eventually be executed.

Phases of event loop

The event loop has six phases each of which has a queue of callbacks to execute. These six phases create one cycle, or loop, which is known as a tick. The event loop executes the callbacks of a specific phase until the queue has been exhausted or the maximum number of callbacks has been executed. The event loop will then move to the next phase in order.

Between each run of the event loop, Node.js checks if it is waiting for any asynchronous I/O or timers and shuts down cleanly if there are not any.

Following are the phases of the event loop which it enters in the respective order.

  1. Timers: Suppose you want to execute a code after 100ms with the help of setTimeout(). The event loop will say, “OK, I am registering your request and I will not execute your code before 100ms for sure. I will execute it after 100ms as soon as possible.”
  2. Pending callbacks: This phase executes callbacks for some system operations such as types of TCP errors. For example if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the pending callbacks phase.
  3. Idle, prepare: This phase is used internally by Node.
  4. Poll: The event loop will execute all the callbacks in poll queue synchronously. If the poll queue was already empty then the event loop will look for requests in the next phase. If there are no requests in the fourth phase then the event loop will wait for callbacks to be added in the poll queue and execute them.
    Once the poll queue is empty the event loop will check for timers whose threshold is over and if it finds any then it will go back to the timers phase and execute the callback for those timers which are over.
  5. Check: The code in this phase is executed using setImmediate() function just after the poll phase. If the event loop finds that the poll queue is empty then it will not wait for poll events and jump directly to the check phase and execute the code there.
  6. Close callbacks: If a socket or handle is closed abruptly (e.g. socket.destroy()), the ‘close’ event will be emitted in this phase. Otherwise it will be emitted via process.nextTick().

Summary

The node.js event loop is single threaded and hence it can perform one task at a time. If a time consuming task is requested from the event loop then it will offload this task to the kernel and the worker threads and continue to work on other requests. There are certain phases of the event loop which it traverses in a fixed order and hence it is able to perform asynchronous and non-blocking tasks.

--

--