Timers, Immediates and Process.nextTick— NodeJS Event Loop Part 2
Deepal Jayasekara
Posted on December 3, 2019
Welcome back to the Event Loop article series! In the first part of the series, I described the overall picture of the NodeJS event loop. In this post, I’m going to discuss in detail about three important queues we discussed in the first article with example code snippets. They are timers, immediates, and process.nextTick callbacks.
Post Series Roadmap
- Event Loop and the Big Picture
- Timers, Immediates and Next Ticks (This article)
- Promises, Next-Ticks, and Immediates
- Handling I/O
- Event Loop Best Practices
- New changes to timers and microtasks in Node v11
Next Tick Queue
Let’s look at the event loop diagram that we saw in the previous post.
Next tick queue is displayed separately from the other four main queues because it is not natively provided by the libuv , but implemented in Node.
Before each phase of the event loop (timers queue, IO events queue, immediates queue, close handlers queue are the four main phases), before moving to the phase, Node checks for the nextTick queue for any queued events. If the queue is not empty, Node will start processing the queue immediately until the queue is empty , before moving to the main event loop phase.
There are some changes introduced in Node v11 which significantly changes this behavior since Node v11. Read more:
New Changes to the Timers and Microtasks in Node v11.0.0 ( and above) | by Deepal Jayasekara | Deepal’s Blog
Deepal Jayasekara ・ ・
Medium
This introduces a new problem. Recursively/Repeatedly adding events to the nextTick queue using process.nextTick function can cause I/O and other queues to starve forever. We can simulate this scenario using the following simple script.
You can see the output is an infinite loop of nextTick callback calls, and the setTimeout, setImmediate and fs.readFile callbacks were never called because any of the ‘ omg!…’ messages were printed in the console.
started
process.nextTick call 1
process.nextTick call 2
process.nextTick call 3
process.nextTick call 4
process.nextTick call 5
process.nextTick call 6
process.nextTick call 7
process.nextTick call 8
process.nextTick call 9
process.nextTick call 10
process.nextTick call 11
process.nextTick call 12
....
You can try setting a finite value as the parameter to addNextTickRecurs and see that setTimeout, setImmediate and fs.readFile callbacks will be called at the end of the process.nextTick call * log messages.
Before Node v0.12, there has been a property called process.maxTickDepth which is used as a threshold to the process.nextTick queue length. This could be manually set by the developers so that Node will process no more than maxTickDepth callbacks from the next tick queue at a given point. But this has been removed since Node v0.12 for some reason. Therefore, for newer Node versions, repeatedly adding events to next tick queue is only discouraged.
Timers queue
When you add a timer using setTimeout or an interval using setInterval, Node will add the timer to the timers heap, which is a data structure accessed through libuv. At the timers phase of the event loop, Node will check the timers heap for expired timers/intervals and will call their callbacks respectively. If there is more than one timer that was expired (set with the same expiration period), they will be executed in the order they were set.
When a timer/interval is set with a specific expiration period, it does not guarantee that the callback will be called exactly after the expiration period. When the timer callback is called depends on the performance of the system (Node has to check the timer for expiration once before executing the callback, which takes some CPU time) as well as currently running processes in the event loop. Rather, the expiration period will guarantee that the timer callback will not be triggered at least for the given expiration time period. We can simulate this using the following simple program.
The above program will start a timer for 1000ms when the program starts and will log how much time it took to execute the callback. If you run this program multiple times, you will notice that it will print a different result each time and it will never print timeout callback executed after 1s and 0ms. You will get something like this instead,
timeout callback executed after 1s and 0.006058353ms
timeout callback executed after 1s and 0.004489878ms
timeout callback executed after 1s and 0.004307132ms
...
This nature of the timeouts can cause unexpected and unpredictable results when setTimeout used along with setImmediate which I’ll explain in the next section.
Immediates Queue
Although the immediates queue is somewhat similar to timeouts on how it behaves, it has some of its own unique characteristics. Unlike timers which we cannot guarantee when its callback gets executed even though the timer expiration period is zero, immediates queue is guaranteed to be processed immediately after the I/O phase of the event loop. Adding an event(function) to the immediates queue can be done using setImmediate function as follows:
setImmediate(() => {
console.log('Hi, this is an immediate');
});
setTimeout vs setImmediate ?
Now, when we look at the event loop diagram at the top of this post, you can see that when the program starts its execution, Node starts processing the timers. And later after processing the I/O, it goes for the immediates queue. Looking at this diagram, we can easily deduce the output of the following program.
As you might guess, this program will always print setTimeout before setImmediate because the expired timer callbacks are processed before immediates. But the output of this program can never be guaranteed! If you run this program multiple times, you will get different outputs.
This is because setting a timer with zero expiration time can never assure that the timer callback will be called exactly after zero seconds. Due to this reason, when the event loop starts it might not see the expired timer immediately. Then the event loop will move to the I/O phase and then to the immediates queue. Then it will see that there is an event in the immediates queue and it will process it.
But if we look at the following program, we can guarantee that the immediate callback will be definitely called before the timer callback.
Let’s see the execution flow of this program.
- At the start, this program reads the current file asynchronously using fs.readFile function, and it provides a callback to be triggered after the file is read.
- Then the event loop starts.
- Once the file is read, it will add the event (a callback to be executed) in the I/O queue in the event loop.
- Since there are no other events to be processed, Node is waiting for any I/O event. It will then see the file read event in the I/O queue and will execute it.
- During the execution of the callback, a timer is added to the timers heap and an immediate is added to the immediates queue.
- Now we know that the event loop is in the I/O phase. Since there are no I/O events to be processed, the event loop will move to the immediates phase where it will see the immediate callback added during the execution of file read callback. Then the immediate callback will be executed.
- In the next turn of the event loop, it will see the expired timer and it will execute the timer callback.
Conclusion
So let’s have a look at how these different phases/queues work altogether in the event loop. See the following example.
After the execution of the above script, the following events are added to the respective event loop queues.
- 3 immediates
- 5 timer callbacks
- 5 next tick callbacks
Let’s now see the execution flow:
- When the event loop starts, it will notice the next tick queue and will start processing the next tick callbacks. During the execution of the second next tick callback, a new next tick callback is added to the end of the next tick queue and will be executed at the end of the next tick queue.
- Callbacks of the expired timers will be executed. Inside the execution of the second timer callback, an event is added to the next tick queue.
- Once callbacks of all the expired timers are executed, the event loop will then see that there is one event in the next tick queue (which was added during the execution of the second timer callback). Then the event loop will execute it.
- Since there are no I/O events to be processed, the event loop will move to the immediates phase and will process the immediates queue.
Great! If you run the above code, you will now get the following output.
this is process.nextTick 1
this is process.nextTick 2
this is process.nextTick 3
this is process.nextTick 4
this is the inner next tick inside next tick
this is set timeout 1
this is set timeout 2
this is set timeout 3
this is set timeout 4
this is set timeout 5
this is process.nextTick added inside setTimeout
this is set immediate 1
this is set immediate 2
this is set immediate 3
Let’s discuss more on next-tick callbacks and resolved promises in the next post. Please feel free to put a response if there’s something to be added to this post or altered.
References:
- NodeJS API Docs https://nodejs.org/api
- NodeJS Github https://github.com/nodejs/node/
- Libuv Official Documentation http://docs.libuv.org/
- NodeJS Design Patterns https://www.packtpub.com/mapt/book/web-development/9781783287314
- Everything You Need to Know About Node.js Event Loop — Bert Belder, IBM https://www.youtube.com/watch?v=PNa9OMajw9w
- Node’s Event Loop From the Inside Out by Sam Roberts, IBM https://www.youtube.com/watch?v=P9csgxBgaZ8
- asynchronous disk I/O http://blog.libtorrent.org/2012/10/asynchronous-disk-io/
- Event loop in JavaScript https://acemood.github.io/2016/02/01/event-loop-in-javascript/
Posted on December 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.