Eventloop in NodeJS: Ways to avoid blocking EventLoop

Jeevan D C
Feb 10, 2021 · 5 min read

Documenting my learning Journey

Contents:
What is Event Loop?
What is Blocking EventLoop?
Let’s block EventLoop
Now, Let’s unblock the event loop
How to avoid event-loop blocking?

What is the Event Loop?

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed. We’ll explain this in further detail later in this topic.

This below diagram shows the order of execution of different phase in event Loop

Analogy:

In a coffee shop, like a Barista(read EventLoop) takes orders of every customers queued(read IOs) and the preparation of coffee, handing out coffee, or packing the take away is done by different different clerks (read workers aka c++ apis in different thread) is how an EventLoop (Single Thread — single cashier) works.

Phases in EventLoop:

  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
  • pending callbacks: executes I/O callbacks deferred to the next loop iteration.
  • idle, prepare: only used internally.
  • poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.
  • check: setImmediate() callbacks are invoked here.
  • close callbacks: some close callbacks, e.g. socket.on('close', ...)
Event Loop Interpretation (Left: JS in browsers, Right: nodejs)

What is Blocking EventLoop?

Let’s block EventLoop

// lets prepare requisites 
touch blockEventLoop.js && npm init -y && npm i fastify -s
blockEventLoop.js

Run the server…

node blockEventLoop.js

Do a ping, and check for a pong!

curl 127.0.0.1:3000/ping

Let’s add another route with an expensive task….

Lets run …

// in Terminal 1
node blocking_event_loop.js
// in Terminal 2
while true; do date; curl 127.0.0.1:3000/ping; sleep 1; echo \n; done;
// in Terminal 3
date && curl 127.0.0.1:3000/block-event-loop && date

As soon as /block-event-loop is called… /ping call stalls. Also check the CPU usage of node process

If example we are using /ping route as simple healthCheck in k8 environment as liveness probe, this will seem like an unhealthy server and pod is restarted.

What really happened here?

Long-running IO (here crypto function) blocked the event loop
// todo insert image

Now, Let’s unblock the event loop:

and run
// in Terminal 1
node blocking_event_loop.js

// in Terminal 2
while true; do date; curl 127.0.0.1:3000/ping; sleep 1; echo \n; done;
// in Terminal 3
date && curl 127.0.0.1:3000/block-event-loop && date
// in Terminal 3
date && curl 127.0.0.1:3000/async-block-event-loop && date

Nothing much changes though even though we are calling the function wrapped in async-await paradigm.

Why?

We are still blocking event loops for it to handle IOs.

Let’s add route /unblock-event-loop

Lets run …

// in Terminal 1
node blocking_event_loop.js
// in Terminal 2
while true; do date; curl 127.0.0.1:3000/ping; sleep 1; echo \n; done;
// in Terminal 3
date && curl 127.0.0.1:3000/unblock-event-loop && date

Now /ping call succeeds without stalling and we are good to handle incoming requests. CPU utilization is also relatively low.

But there is a caveat…. it takes longer to complete /unblock-event-loop call. That’s the tradeoff we pay but we didn’t block other calls.

Why did calling setTimeout (/ setImmedidate/ process.nextTick) unblock the event loop?

Because of order through which setTimeout, setImmedidate, Promise, callbacks, etc are called in event loop.

https://medium.com/@noobj/eventloop-in-nodejs-macrotasks-and-microtasks-164417e619b9

In our example microtask (hash.update function) added more and more tasks to queue, with this event loop didn’t get a chance to handle incoming request (network IO). By calling setTimeout we kindof gave breathing space to execute macro tasks like (network IO call) to execute with completing event loop for each hash.update call made.

How to avoid event-loop blocking?

  • Long/sync ops (http, db, file ops. Etc).
    Ex: 1. Sync file reads of large files, blocking db calls
  • Timers usage (setImmediate, setTimeout, process.nextTick ) Ex: Timers Getting created but not getting destroyed.
  • Open sockets/ too many sockets creation.
    Ex:1 Creating new connection for every API call for intra MS communication => fix keep-alive header usage with max connections
    2. Db connection pools
  • DNS queries. Ex: DNS TTL is too low and we need afresh resolve very frequently
  • Crypto functions. Ex: Long awaiting crypto functions with large data
  • Compressing functions
  • Large Sync JSON ops. Ex: JSON.parse(…large data…)
  • Lengthy regex ops

Additional reads:

Like to tinker and learn through trials and errors? We might use your quirky mind in our team! Join us in making the next life-centric digital solutions

Dkatalis

building growth and excellence, enabled by technology