The Node JS Event Loop

Rotem Zaig
payu-engineering
Published in
6 min readNov 14, 2019

In this blog, I will cover how Node.js uses the event loop when executing a program.

This is the first part out of two articles:
1. The Node JS event loop (This article)
2. Promises and nextTick

Most of us Node.js (or JS) developers, know that “Node.js is a single threaded non-blocking I/O event driven language…” but what does this mean??

“single threaded’

It runs on a single main thread, which means that every js code being executed is blocking the next code to be executed.

“non-blocking I/O“

Means that I/O operations (despite the single threaded thing...) aren’t blocking the thread.

“event driven“

Execute callbacks whenever an event is triggered (finished I/O, or timeout, etc…). This mechanism is what helps node to be non-blocking.

NodeJS uses the OS (and its threads) in order to perform async tasks (such as setTimeout, http requests, read file, write file) that would have blocked the thread if waited for. NodeJS uses events to let the program know when these async tasks are done.

So how does Node.js manage to do that?

Node.js uses the V8 engine to run javascript and the Libuv library to communicate with the OS.

Libuv also implements the Event Loop, which is responsible for the ‘non- blocking I/O’ and ‘event driven’ mechanism.

Taken from libuv 1.30.2-dev documentation »

“Libuv is a cross-platform support library which was originally written for Node.js. It’s designed around the event-driven asynchronous I/O model.

The library provides much more than a simple abstraction over different I/O polling mechanisms: ‘handles’ and ‘streams’ provide a high level abstraction for sockets and other entities; cross-platform file I/O and threading functionality is also provided, amongst other things.”

So what is the Event Loop and how does it work?

The event loop is a sequence of phases that are processed one after another.
Each phase has its own callbacks queue (for example, the Timers phase queue will have setTimeout and setInterval callbacks).
After executing all the callbacks in a phase, the loop will continue to the next phase. When the queues in all phases are empty, the event loop will exit and we are done.

NOTE: a phase could also end if too many callbacks exist in the queue. For example, if 101 callbacks are in the queue and the limit is 100, then 100 callbacks will be executed immediately. It will then continue to the next phase and the additional 1 callback will be executed in the next loop.

The diagram below illustrates the flow of a Node.js program:

When Node.js starts, it initializes the event loop.

When the initial program finishes, one of the following happens:

  • If there were only sync operations, the program will exit.
  • If there were async operations, the program will enter the event loop, starting with the timers phase.
  • If all phases are done and there are more callbacks waiting in the event loop, another loop will be executed starting again from the timers phase.

When there are no more callbacks in all phases of the event loop, the event loop will exit and the program will finish.

Let’s dive a bit deeper into each phase:

Timers:

In this phase, all callbacks of timers, whose time has passed, will be executed.

Timers are stored in a min-heap. All timers are ordered by their moment of execution. When the timer phase starts, it calculates the current time and compares it with the first timer. If the timer threshold passed, it will execute its callback and check the next timer and do the same, until it reaches a timer whose time didn’t pass.

Pending callbacks:

Responsible for handling errors returned from OS related actions.

Idle, prepare:

Used internally by Libuv.

poll I/O:

Responsible for networking and file system I/O callbacks (http.get, fs.readFile, etc.).

Pending the I/O callback, the following polling rules are applied:

  1. If there are no more callbacks in other phases, stay in poll phase and wait for the I/O callback.
  2. If callbacks exist in other phases (and the I/O operation still isn’t completed), continue the loop to let them be executed.
  3. If a callback exists on the timers phase that should be executed in the future, the poll phase will wait until the soonest timer’s threshold is reached. If by then, the I/O operation hasn’t yet completed, the poll phase will wait for the next loop.
    For example, a callback exists on the timers phase and should be executed in 100ms from now. The poll will wait 100ms to give the I/O operation a chance to return the data. If the I/O operation didn’t complete after 100ms, the loop will continue, execute the timer callback and will return to the poll phase in the next iteration for another try.

check:

The phase where all callbacks assigned by setImmediate will be executed unconditionally.

close callbacks:

Closing callbacks for open sockets, open files etc. (such as fs.close)

Let’s take a look at an example of a program execution:
(note that the fs.readfile callback is called last because we assume that I/O operations take a long time)

The node.js event loop execution example

Let’s test your knowledge with a couple of examples:

what will be printed here?

while (i < 3) { i++; setImmediate(() => console.log(‘setImmediate cb’)); setTimeout(() => console.log(‘setTimeout cb’), 0);}

Solution:

Explanation:

As we explained, assigning timers includes registration and calculations, which make the operation of deciding which timer has passed, longer. That’s why sometimes even callbacks with a timeout of 0 will be executed after the setImmediate callbacks, which are executed unconditionally.

What will be printed here?

fs.readFile(‘file.txt’, () => { setTimeout(() => console.log(‘setTimeout’), 0); setImmediate(() => console.log(‘setImmediate’));});

Solution:

setImmediatesetTimeout

Explanation:

The first callback to happen here is of course the I/O callback. After the callback completed and before leaving the poll I/O phase, there will still be callbacks waiting in the check (setImmediate) and the timers phases .The next phase after poll I/O is setImmediate. Callbacks in the setImmediate phase are unconditionally executed, which will guarantee that the setImmediate callbacks will be executed before the setTimeout callback.

Nice Example to show the differences between setImmediate and setTimeout:

function foo(recursionFunc) {
const start = new Date();
let i = 0;
function bar() {
i += 1;
if (i < 1000) {
if (recursionFunc === 'setImmediate') {
setImmediate(bar);
} else {
setTimeout(bar, 0);
}
} else {
const duration = new Date() - start;
console.log(duration);
}
}
bar();
}
// foo('setImmediate');
// output will be approximately 4ms
// foo('setTimeout');
// output will be approximately 1400ms

Explanation:

Here we are executing two functions recursively. In the first run, we use setImmediate for the recursion. In the second run we use setTimeout.

The first run took an average of 4ms while using setTimeout took 1400ms. This is due to the calculation needed for each timer (which also causes a large number of loops), while the setImmediate callback will run unconditionally and without calculations all in the same phase.

In the next blog, i will write about next-tick and promises and we will see how they are integrated in the program execution process.

References:

--

--

payu-engineering
payu-engineering

Published in payu-engineering

Where our payments engineers share their tips on building the leading payments platform for growth markets.

Rotem Zaig
Rotem Zaig

No responses yet