Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

A complete guide to the Node.js event loop

Tags: loop phase queue

Posted on Jul 19 • Originally published at blog.logrocket.com Written by Joseph Mawa✏️Node.js is a single-threaded, non-blocking, event-driven JavaScript runtime environment. The Node runtime environment enables you to run JavaScript outside the browser on the server side. The asynchronous and non-blocking feature of Node.js is primarily orchestrated by the event Loop. In this article, you will learn the Node.js event loop so that you can leverage its asynchronous APIs to build efficient Node.js applications. Knowing how the event loop works internally will not only help you write robust and performant Node.js code, but it will also teach you to debug performance issues effectively. Jump ahead:The Node.js event loop is a continuously running, semi-infinite loop. It runs for as long as there is a pending asynchronous operation. Starting a Node.js process using the node command executes your JavaScript code and initializes the event loop. If 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. Most I/O operations, such as reading and writing to file, file encryption and decryption, and networking, are time-consuming and computationally expensive. Therefore, to avoid blocking the main thread, Node.js offloads these operations to the native system. There, the Node process is running, so the system handles these operations in parallel. Most modern operating system kernels are multi-threaded by design. Therefore, an operating system can handle multiple operations concurrently and notifies Node.js when those operations are complete. The event loop is responsible for executing your asynchronous API callbacks. It has six major phases:Though the above list is linear, the event loop is cyclical and iterative, as in the diagram below: After the event loop’s last phase, the next iteration of the event loop starts if there are still pending events or asynchronous operations. Otherwise, it exits, and the Node.js process ends. We will explore each phase of the event loop in detail in the following sections. Before that, let's explore the “next tick” and microtask queues that appear at the center of the event loop in the above diagram. Technically, they are not part of the event loop.Promises, queueMicrotask, and process.nextTick are all part of the asynchronous API in Node.js. When promises settle, queueMicrotask and the .then, .catch, and .finally callbacks are added to the microtask queue. On the other hand, the process.nextTick callbacks belong to the “next tick” queue. Let's use the example below to illustrate how the microtask and the “next tick” queues are processed:Let’s assume the three timers above expire at the same time. When the event loop enters the timers phase, it will add the expired timers to the timers callback queue and execute them from the first to the last: In our example code above, when executing the first callback in the timers queue, the .then, .catch, and queueMicrotask callbacks are added to the microtask queue. Similarly, the process.nextTick callback is added to a queue that we will refer to as the “next tick” queue. Be aware, console.log is synchronous. When the first callback from the timers queue returns, the “next tick” queue is processed. If more "next ticks" are generated while processing callbacks in the “next tick” queue, they are added to the back of the “next tick” queue and executed as well. When the “next tick” queue is empty, the microtask queue is processed next. If the microtasks generate more microtasks, they are also added to the back of the microtask queue and executed. When both the “next tick” queue and microtask queue are empty, the event loop executes the second callback in the timers queue. The same process continues until the timers queue is empty: The process described above is not limited to the timers phase. The “next tick” queue and the microtask queue are processed similarly when the event loop executes JavaScript in all the other major phases.As explained above, the Node.js event loop is a semi-infinite loop with six major phases. It has more phases, but the event loop uses some of the phases for housekeeping internally. They have no direct effect on the code you write. Therefore, we won't cover them here. Each major phase in the event loop has a first-in-first-out queue of callbacks. For example, 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. We’ll explore the major phases of the event loop in the sections below.Like the browser, Node.js has the timers API for scheduling functions that will execute in the future. The timers API in Node.js is similar to that in the browser. However, there are some slight implementation differences. The timers API consists of the setTimeout, setInterval, and setImmediate functions. All three timers are asynchronous. The timers phase of the event loop is only responsible for handling setTimeout and setInterval. On the other hand, the check phase is responsible for the setImmediate function. We will explore the check phase later. Both setTimeout and setInterval have the function signature below:With setTimeout, callback is invoked once when delay elapses. On the other hand, setInterval schedules callback to run every delay milliseconds. The diagram below shows the event loop after removing all the phases except the timers phase: For simplicity, let's take three scheduled setTimeout that expire concurrently. The steps below describe what happens when the event loop enters the timer phase:In the steps above, we used a queue of three expired timers. However, that will not always be the case in practice. 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. The event loop is blocked when executing JavaScript callbacks. If a callback takes long to process, the event loop will wait until it returns. Because Node.js mostly runs on the server side, blocking the event loop will lead to performance issues. Similarly, 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.During the polling phase, which we will cover shortly, the event loop polls for events such as file and network I/O operations. The event loop processes some of the polled events within the poll phase and defers specific events to the pending phase in the next iteration of the event loop. In the pending phase, the event loop adds the deferred events to the pending callbacks queue and executes them. Events processed in the pending callbacks phase include certain TCP socket errors emitted by the system. For example, some operating systems defer the handling of the ECONNREFUSED error events to this phase.The event loop uses the idle, prepare phase for internal housekeeping operations. It doesn't have a direct effect on the Node.js code you write. Though we won't explore it in detail, it is necessary to know that it exists.The poll phase has two functions. The first is to process the events in the poll queue and execute their callbacks. The second function is to determine how long to block the event loop and poll for I/O events. When the event loop enters the poll phase, it queues pending I/O events and executes them until the queue is empty or a system-dependent limit is reached. In between the execution of the JavaScript callbacks, the "next tick" and microtask queues are drained, like in the other phases. The difference between the poll phase and other phases is that the event loop will sometimes block the event loop for a duration and poll for I/O events until the timeout elapses, or after reaching the maximum callback limit. The event loop considers several factors when deciding whether to block the event loop and for how long to block it. Some of these factors include the availability of pending I/O events and the other phases of the event loop, such as the timers phase: The event loop executes the setImmediate callback in the check phase immediately after I/O events. setImmediate has the function signature below:The event loop executes multiple setImmediate callbacks in the order in which they are created. In the example below, the event loop will execute the fs.readFile callback in the poll phase because it's an I/O operation. After that, it executes the setImmediate callbacks immediately in the check phase within the same iteration of the event loop. On the other hand, it processes the setTimeout in the timers phase in the next iteration of the event loop. When you invoke the setImmediate function from an I/O callback like in the example below, the event loop will guarantee it will run in the check phase within the same iteration of the event loop:Any microtasks and "next ticks" generated from the setImmediate callbacks in the check phase are added to the microtask queue and “next tick” queue respectively and drained immediately like in the other phases.This close phase is where Node.js executes callbacks to close events and winds down a given event loop iteration. 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. It is worth emphasizing that 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.As hinted above, it is important to understand the Node.js event loop in order to write performant, non-blocking asynchronous code. Using the asynchronous APIs in Node.js will run your code in parallel, but your JavaScript callback will always run on a single thread. Therefore, you can unintentionally block the event loop while executing the JavaScript callback. Because Node.js is a server-side language, blocking the event loop will make your server slow and unresponsive, reducing your throughput. In the example below, I am deliberately running a while loop for about a minute to simulate a long-running operation. When you hit the /blocking endpoint, the event loop will execute the app.get callback in the poll phase of the event loop:Because the callback is executing a time-consuming operation, the event loop is blocked for the duration the task is running. Any request to the /non-blocking route will also wait for the event loop to first unblock. As a result, your application will become unresponsive. Your requests from the frontend will become slow and eventually timeout. To perform such CPU-intensive operations, you can take advantage of the worker thread. Similarly, do not use the synchronous APIs for the following modules on the server side because they can potentially block the event loop:As explained above, Node.js runs JavaScript code in a single thread. However, it has worker threads for concurrency. To be precise, in addition to the main thread, Node has a thread pool comprised of four threads by default. Libuv, the C library that gives Node.js its asynchronous, non-blocking I/O capability is responsible for managing the thread pool. Node.js gives you the capability of using additional threads for computationally expensive and long-lasting operations to avoid blocking the event loop.Node promises do not run on a separate thread. The .then, .catch, and .finally callbacks are added to the microtask queue. As explained above, the callbacks in the microtask queue are executed on the same thread in all the major phases of the event loop.The event loop orchestrates the asynchronous and non-blocking feature of Node. It is responsible for monitoring client requests and responding to requests on the server side. If a JavaScript callback blocks the event loop, your server will become slow and unresponsive to client requests. Without the event loop, Node.js would not be as powerful as it is now, and Node.js servers would be painfully slow.Node.js has several built-in synchronous and asynchronous APIs. Synchronous APIs block the execution of JavaScript code until the operation is complete. In the example below, we’re using fs.readFileSync to read file contents. fs.readFileSync is synchronous. Therefore, it will block the execution of the rest of the JavaScript code until the file reading process is complete before moving to the next line of code:On the other hand, non-blocking asynchronous APIs perform the operations in parallel by offloading them to the thread pool or the native system on which Node.js is running. When the operation is complete, the event loop schedules and executes the JavaScript callback. For example, the async form of the fs module uses the thread pool to write or read file contents. When the contents of the file operation are ready for processing, the event loop executes the JavaScript callback in the poll phase. In the example below, fs.readFile is async and non-blocking. The event loop will execute the callback you pass to it when the file read operation is complete. The rest of the code will run without waiting for the file operation to complete:Microtasks are executed between operations in all the major phases of the event loop. Each major phase of the event loop executes a queue of JavaScript callbacks. Between executions of successive JavaScript callbacks in the phase queue, there is a microtask checkpoint where the microtask queue is drained.The Node.js event loop runs for as long as there are pending events for it to process. If there isn't any pending work, the event loop exits after emitting the exit event, and the exit listener callback returns. You can also exit the event loop by explicitly using the process.exit method. Invoking process.exit will immediately exit the running Node.js process. Any pending or scheduled events in the event loop will be abandoned:You can listen for the exit event. However, your listener function must be synchronous because the Node.js process will exit immediately after the listener function returns.The Node runtime environment has APIs to write non-blocking code. However, because all your JavaScript code executes on a single thread, it is possible to block the event loop unintentionally. An in-depth knowledge of the event loop helps you write robust, secure, and performant code and effectively debug performance issues. The event loop has about six major phases. These six phases are the timers, pending, idle and prepare, poll, check, and close. Each phase has a queue of events that the event loop processes until it is empty or reaches a hard system-dependent limit. While executing a callback, the event loop is blocked. Therefore, be sure your asynchronous callbacks don't block the event loop for long, otherwise your server will become slow and unresponsive to client requests. You can use the thread pool to perform long-running or CPU-intensive tasks.Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Shambashib Majumdar - Jul 14 Matteo Bruni - Jul 12 Emanuele Bartolesi - Jul 3 Paul Asjes - Jul 14 Rather than spending hours/days trying to reproduce an elusive bug, you can see the reproduction in seconds with LogRocket. Demo it yourself — no signup required. Once suspended, logrocket will not be able to comment or publish posts until their suspension is removed. Once unsuspended, logrocket will be able to comment and publish posts again. Once unpublished, all posts by logrocket will become hidden and only accessible to themselves. If logrocket is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Matt Angelosanto. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag logrocket: logrocket consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging logrocket will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

A complete guide to the Node.js event loop

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×