Never Block the Event Loop

Never Block the Event Loop

Daily short news for you
  • Here! A question that I've been wondering about for ages, and until yesterday, everything was finally clarified.

    Normally, people use height: 100vh to set the height equal to the viewport of the screen. On computers, there's no issue, and even simulating the size of a smartphone works fine. But when opened on a phone, height: 100vh always exceeds the viewport. Huh!? What's going on???

    The explanation for this is that mobile devices have a different way of calculating the viewport compared to computers. It is often interfered with or affected by the address bar and navigation bar of the platform you are using. Therefore, if you want 100vh on mobile to truly equal the viewport, you need to take an additional step to reset the viewport.

    It's easy, first, you need to create a CSS variable --vh right in the script tag at the top of the page.

    function updateViewportHeight() { const viewportHeight = globalThis.innerHeight; document.documentElement.style.setProperty('--vh', `${viewportHeight * 0.01}px`); } updateViewportHeight(); window.addEventListener('resize', updateViewportHeight);

    Then instead of using height: 100vh, change it to height: calc(var(--vh, 1vh) * 100). And that's it.

    » Read more
  • Nowadays, how much memory will 1 million (1M) concurrent tasks consume? That is the question posed by hez2010, and he decided to find the answer by experimenting with a simple program in various programming languages: How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?

    To summarize, Rust remains unbeatable, but the second position surprised me 😳

    » Read more
  • Is something coming? 😱🥶

    » Read more

Issues

All requests from receiving to responding go through the Event Loop. This means that if the Event Loop spends too much time at any point, all current requests and new requests will be blocked.

We should ensure that we never block the Event Loop. In other words, each callback function should complete as quickly as possible. This also applies to await, Promise.then, etc.

A good way to ensure this is to consider the "algorithmic complexity" of your callback functions. If a callback function doesn't care about the input size, we can ensure "fairness" for each request. If callbacks perform different processing steps depending on their arguments, then we should be concerned about the worst-case scenario of how much time it takes.

Here's an example of a request that doesn't care about the input:

app.get('/constant-time', (req, res) => {
  res.sendStatus(200);
});

And here's an example of a request where the processing time depends on the input:

app.get('/countToN', (req, res) => {
  const n = req.query.n;
  for (let i = 0; i < n; i++) {
    // do something on each iteration
  }
  res.sendStatus(200);
});

Node.js uses the V8 Engine, which is pretty fast for many common operations. However, it also has some exceptional cases when it comes to working with regexps or JSON.

REDOS: Denial of Service attacks with regexp patterns

A common way to block the Event Loop is to use a "vulnerable" regexp pattern. That's why we should avoid using vulnerable regexps.

Understandably, sometimes we need to use regexps to determine or search for a certain string. Unfortunately, in some cases, combining regex patterns can take an exponential amount of time depending on the input string.

A vulnerable regexp pattern is a regexp pattern that can take an exponential amount of time, leading to REDOS. Determining whether regexp patterns are truly vulnerable or not is a difficult question, and it depends on whether you are using Perl, Python, Ruby, Java, JavaScript, etc., but here are some rules that apply to all of these languages:

  • Avoid nested quantifiers, such as `. The V8 regexp engine can handle some of them quickly, but others are vulnerable. (a+)*
  • Avoid OR with nested disjunctions, such as . Again, these can be fast some of the time. (a|a)*
  • Avoid using backreferences, such as . No regexp engine can guarantee evaluating those in linear time. (a.*) \1
  • If you are performing simple string matching, use or local equivalents. It will be cheaper and will never take more than .indexOfO(n)

If you are unsure whether your regexp pattern is vulnerable or not, keep in mind that Node.js typically doesn't have issues reporting match results even for vulnerable regexps and long input strings. The exponential behavior is activated when there is a mismatch but Node.js cannot be certain until it tries many paths through the input string.

There are some tools to check the safety of regexp patterns:

However, they won't necessarily catch all vulnerable regexps.

Another approach is to use a different regexp engine. You can use the node-re2 module, which uses the fast regexp engine RE2 by Google. But be warned, RE2 is not 100% compatible with V8 regexps, so test your regexps when swapping in the node-re2 module to handle your regexps. And complex regexps are not supported by node-re2.

Time-consuming Core Modules

Certain core modules of Node.js have "expensive" synchronous APIs, including:

  • Encryption
  • Compression
  • File System
  • Child Process

These APIs are expensive because they involve significant computation (encryption, compression), require I/O (file I/O), or have the potential for both (child processes). These APIs are designed for convenience scripting, but not intended for use in a server context. If you execute them on the Event Loop, they will take longer to complete compared to regular JavaScript constructs, blocking the Event Loop.

In a server, you should not use the following synchronous APIs from these modules:

Encryption:

  • crypto.randomBytes (sync)
  • crypto.randomFillSync (sync)
  • crypto.pbkdf2Sync (sync)

You should also be cautious when providing large input to encryption and decryption routines.

Compression:

  • zlib.inflateSync
  • zlib.deflateSync

File System:

Avoid using synchronous file system APIs. For example, if the files you access are in a distributed file system like NFS, the access time may vary significantly.

Child Process:

  • child_process.spawnSync
  • child_process.execSync
  • child_process.execFileSync

JSON DOS

JSON.parse is also an "expensive" operation. It depends on the length of the input data, so it can unexpectedly take a long time. JSON.stringify also has the same behavior, with a complexity of up to O(n).

If your server deals with JSON objects, especially processing data received from a client, you should be mindful of their size.

For example, let's create a string object with a size of 2^21, and then JSON.parse it. The string has a size of 50MB. It takes 0.7 seconds to stringify the object, 0.03 seconds to indexOf, and 1.3 seconds to parse the string.

var obj = { a: 1 };
var niter = 20;

var before, str, pos, res, took;

for (var i = 0; i < niter; i++) {
  obj = { obj1: obj, obj2: obj };
}

before = process.hrtime();
str = JSON.stringify(obj);
took = process.hrtime(before);
console.log('JSON.stringify took ' + took);

before = process.hrtime();
pos = str.indexOf('nomatch');
took = process.hrtime(before);
console.log('Pure indexof took ' + took);

before = process.hrtime();
res = JSON.parse(str);
took = process.hrtime(before);
console.log('JSON.parse took ' + took);

To mitigate this, there are some npm modules that provide async JSON APIs like:

  • JSONStream.
  • Big-Friendly JSON, which has stream APIs as well as async versions of standard JSON APIs using the partitioning-on-the-Event-Loop model.

Conclusion

The above article has presented some seemingly simple behaviors that have a significant impact on the Event Loop. In the next article, we will explore together the solutions to handling Event Loop "blocking".

Premium
Hello

5 profound lessons

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

View all

Subscribe to receive new article notifications

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

Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.

Did you find this article helpful?
NoYes

Comments (0)

Leave a comment...