skip to content
Profile image
Dharmendra Kashaudhan

Event Loop: The heart of JavaScript & Node.js

/ 5 min read

If you are a node.js dev, you must have wondered how Node.js manages to handle multiple concurrent requests without blocking, despite being single-threaded. It’s impressive how efficient it is at managing various I/O intensive tasks, including network applications and web servers.

Wonder no more, you have come to the right place. In this blog we’ll take a deep dive into how does it all happens.

Why node is single threaded

Node.js was originally an experiment in asynchronous processing. The idea behind it was to explore whether performing asynchronous processing on a single thread could offer better performance and scalability under typical web loads compared to the traditional thread-based implementation.

And when Node.js isn’t doing CPU intensive task it can handle thousands of concurrent connections more than IIS or Apache on other thread based servers.

The asynchronous, single-threaded nature of Node.js does introduce complexity, but compared to threading, it presents a different set of challenges. A single race condition can have significant repercussions, such as depleting the thread pool or slowing response times due to misconfigured settings. Additionally, multithreading brings its own complexities, including deadlocks and priority inversions.

What is event loop

Event loop is a programming design pattern that waits for and dispatches events or messages within a program. It works by making a request to a event provider which generally blocks the event until an event has arrived then dispatches the relevant event.

Whenever you start a node.js instance it initializes a event loop, & when node.js encounters an asynchronous operation such as timers, files, and network I/O while executing a script, it offloads the operation to the native system or the thread pool. The event loop is responsible for executing those asynchronous API callbacks. It has six major phases:

  1. Timers phase for handling setTimeout and setInterval
  2. Pending callbacks phase for executing deferred callbacks
  3. Idle, Prepare phase that the event loop uses for internal housekeeping
  4. Poll phase for polling and handling events such as file and network I/O
  5. Check phase for executing setImmediate callbacks
  6. Close phase for handling certain close events

We’ll explore each phase in detail. But first let’s see what is process.nextTick & micro tasks are which appears in the event loop, but technically they are not part of event loop.

Promises, queueMicrotask, and process.nextTick are integral components of Node.js’s asynchronous API. Upon settling, promises add queueMicrotask and the .then, .catch, and .finally callbacks to the microtask queue.

Phases of event loop

Each phase in the event loop has a FIFO queue of callbacks. The operating system will run scheduled timers until they expire. After that, the expired timers are added to the timers callback queue. The event loop then executes the callbacks in the timers queue until the queue is empty or when a maximum number of callbacks is reached.

  1. Timer phase: Timer APIs are scheduled functions that expire in future such as setTimeout, setInterval & setImmediate & all three are asynchronous. Timer phase is only responsible for setTImeout & setInterval whereas check phase is responsible for setImmediate. The event loop will process the timers queue until it is empty or the maximum number of callbacks is reached before moving to the next phase.
    When executing JavaScript callbacks, the event loop becomes blocked. If a callback requires significant processing time, the event loop will remain stalled until it completes. Given that Node.js primarily operates on the server side, such blocking of the event loop can result in performance degradation.

    Keep in mind that the delay argument you pass to the timer functions is not always the exact waiting time before executing the setTimeout or setInterval callback. It is the minimum waiting time. The duration it takes depends on how busy the event loop is, and the system timer used.

  2. Pending callbacks: The event loop appends deferred events to the pending callbacks queue and proceeds to execute them. Events handled during the pending callbacks phase encompass specific TCP socket errors generated by the system. For instance, certain operating systems defer the handling of ECONNREFUSED error events until this phase.

  3. Idle, prepare: The event loop employs the idle and prepare phase for internal housekeeping tasks. While these phases don’t directly impact the Node.js code you write.

  4. Pool Phase: This phase determines how long to block the event loop and poll for I/O events & it process the events in the poll queue and execute their callbacks. In this the the pending I/O events are queued and executes then until the queues are empty.

  5. Check: This phase handles the setImmediate callback execution. It is executed in the order in which the are created. Any microtasks and nextTicks generated from the setImmediate callbacks in the check phase are added to the microtask queue and nextTick queue respectively and drained immediately like in the other phases.

  6. Close callbacks: This phase executes the callback to close the event. When a socket closes, the event loop will process the close event in this phase. If “next ticks” and microtasks are generated in this phase, they are processed like in the other phases of the event loop.
    You can terminate the event loop at any phase by invoking the process.exit method. The Node.js process will exit, and the event loop will ignore pending asynchronous operations.

process.nextTick

It is part of the asynchronous API.

As mentioned in the node.js docs:

any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues. This can create some bad situations because it allows you to “starve” your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.