Understanding the Event Loop in node.js

Understanding the Event Loop in node.js

Daily short news for you
  • swapy is a library that helps you create drag-and-drop actions to easily swap the positions of components.

    The library supports a wide range of platforms, making it perfect for building something personalized like a user’s Dashboard.

    » Read more
  • After waking up, I saw the news feed flooded with articles about Microsoft rewriting the TypeScript compiler - tsc in Go, achieving performance that is 10 times faster than the current one. Wow!

    But when I saw this news, the question immediately popped into my head: "Why not Rust?" You know, the trend of rewriting everything in Rust is hotter than ever, and it’s not an exaggeration to say that it's sweeping through the rankings of previous legacy tools.

    What’s even more interesting is the choice of Go - which supposedly provides the best performance to date - as they say. Many people expressed disappointment about why it wasn’t C# instead 😆. When something is too famous, everything is scrutinized down to the smallest detail, and not everyone listens to what is said 🥶

    Why Go? #411

    » Read more
  • I read this article Migrating Off Oh-My-Zsh and other recent Yak Shavings - the author essentially states that he has been using Oh-My-Zsh (OMZ) for a long time, but now the game has changed, and there are many more powerful tools that have come out, so he realizes, oh, he doesn't need OMZ as much anymore.

    I also tried to follow along because I find this situation quite similar to the current state. It was surprising to discover something new. I hope to soon write a post about this next migration for my readers 😁

    » Read more

The event loop can be called the heart of Node.js. It is used to handle asynchronous I/O tasks in Node.js. So how is it structured and how does it work? Let's find out in the following article.

What is the Event Loop?

The event loop allows Node.js to perform asynchronous I/O tasks, even though JavaScript is single-threaded by actually offloading operations to the operating system whenever possible.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel notifies Node.js so that the attached callback function can be added to the poll queue and eventually executed.

How does the Event Loop work?

When Node.js starts, it initializes the Event Loop, processes any input script that it has been provided (or an REPL), which may include performing some asynchronous functions, schedulling timers, or process.nextTick(), and after that starts processing the Event Loop.

The following diagram provides a high-level overview of the sequence of events in the Event Loop.

Các pha của event loop

Note: each block is considered as one "phase" of the event loop.

Each phase has a FIFO queue of callbacks. Each phase has a specific task, but generally, when the Event Loop steps into a specific phase, it will process any data for that phase and then execute the callbacks in the queue for that phase until it is empty or reaches the execution limit. Then the Event Loop moves on to the next phase.

Since each phase can have a large number of callbacks waiting to be processed, some of the callbacks for timers may have a longer wait time than the initial threshold set, as the initial time threshold only guarantees the shortest wait time, not the exact waiting time.

For example,

setTimeout(() => console.log('hello world'), 1000);

The 1000ms is the shortest waiting time, but it doesn't mean that the console.log statement will be executed exactly after 1000ms.

Overview of Event Loop phases

  • Timers: Executes callbacks scheduled by setTimeout() and setInterval().
  • Pending callbacks: Executes I/O callbacks deferred to the next loop iteration.
  • Idle, prepare: Used for internal purposes by Node.js.
  • Poll: Retrieves new I/O events, executes related callbacks (almost all with the exception of close callbacks, timer callbacks, and setImmediate()).
  • Check: Executes setImmediate() callbacks.
  • Close callbacks: Executes close callbacks, e.g. socket.on("close").

Between each iteration of the Event Loop, Node.js checks if it is waiting for any asynchronous I/O or timers and exit if there is none.

Detailed Event Loop phases

Timers

A timer specifies a threshold after which a callback can be executed. The timer callbacks will run as soon as possible after the specified amount of time has passed. However, they can also be delayed for some time.

Note: Technically, poll controls when the timers are executed.

For example, let's say we set up a setTimeout() that executes after 100ms and then run an someAsyncOperation function that asynchronously reads a file and takes 95ms to complete:

const fs = require('fs');

function someAsyncOperation(callback) {
  // assume reading a file takes 95ms
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms`);
}, 100);

// someAsyncOperation takes 95ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // event loop will be delayed by 10ms...  
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

When the Event Loop steps into the poll phase and there are no callbacks for timers, one of the two things happens:

  • If the poll queue is not empty, the Event Loop will iterate through its callbacks and execute them one by one until the queue is empty or it reaches the system-specified limit.
  • If the poll queue is empty, one of the two things happens:
  • If there are setImmediate() callbacks scheduled, the Event Loop will exit the poll phase and proceed to the check phase to execute those scheduled callbacks.
  • If there are no setImmediate() callbacks scheduled, the Event Loop will wait for callbacks to be added to the queue and then execute them immediately.

When the poll queue is empty, the Event Loop checks if any timers have reached their threshold for execution. If one or more timers are ready, the Event Loop will go back to the timers phase to execute those callbacks.

Pending callbacks

This phase executes callback functions for some system operations, such as certain types of TCP errors. For example, if a TCP socket receives ECONNREFUSED when trying to connect, some *nix systems want to wait to report the error. It will be queued here to await its turn.

Poll

The poll phase has two main functions:

  • Calculate how long it should block and poll for I/O events, then:
  • Handle events from the poll queue

When the Event Loop steps into the poll phase and there are no timer callbacks, one of two things will happen:

  • If the poll queue is not empty, the Event Loop will iterate through its callbacks and execute them one by one until the queue is empty or it hits the limit of the system.
  • If the poll queue is empty, one of two things will happen:
    • If there are scheduled commands by setImmediate(), the Event Loop will exit the poll phase and proceed to the check phase to execute those scheduled commands.
    • If there are no commands scheduled by setImmediate(), the Event Loop waits for callbacks to be added to the queue and then executes them immediately.

When the poll queue is empty and there are no setImmediate() callbacks scheduled, the Event Loop checks if any timers have reached their execution threshold. If one or more have, the Event Loop will go back to the timers phase to execute those callbacks.

Check

This phase allows us to execute callbacks immediately after the poll phase completes. If the poll phase is idle and there are setImmediate() callbacks scheduled, the Event Loop can proceed to this phase instead of waiting for poll events.

setImmediate() is a special timer that runs in a separate phase of the Event Loop. It uses libuv API to schedule the execution of callbacks after the poll phase completes.

In general, once the code is executed, the Event Loop eventually reaches the poll phase - where it will wait for incoming connections, requests, etc... However, if a callback is scheduled by setImmediate() and the poll phase is in an idle state, it will end and continue to the check phase instead of waiting for the poll events.

Close callback

If a socket or handle is closed unexpectedly (e.g., socket.destroy()), then the 'close' event will be emitted in this phase. Otherwise, it will be emitted via process.nextTick().

Summary

The Event Loop in Node.js is implemented using libuv and consists of 6 phases, each handling a separate part of the work. Knowing this, we can explain the priority order of executing callbacks for functions like setTimeout, setImmediate, or process.nextTick. Regarding the priority order and the benefits of each, I will address this in another article. See you later!

Premium
Hello

The secret stack of Blog

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (1)

Leave a comment...
Avatar
Ẩn danh3 months ago
Chơi dịch từ nodejs à, có hiểu k mà viết bài v cha
Reply
Avatar
Xuân Hoài Tống3 months ago
Đúng là bài này mình dịch từ nodejs, từ khá lâu rồi và chưa có cập nhật gì thêm. Mình sẽ cố gắng hoàn thiện bài viết hơn nữa.