How does the Node JS runtime environment work?

Raj Jain
Credera Engineering
10 min readAug 29, 2023
Node JS

JavaScript (JS), arguably the most popular and in-demand programming language, powers the web by making websites user-friendly, interactive, and responsive. 2009 was a turning point for software engineers, when Ryan Dahl pioneered the use of JavaScript on server-side (back-end) applications through Node JS. Up until this point, JavaScript could only be used to develop client-side (front-end) applications.

There are a whole host of three-hour video tutorials available on YouTube to help you get started on developing backend applications through Node JS today. However, it by truly understanding how Node JS works behind the scenes that you can set yourself apart from the competition, as it could give you better insights about your code performance.

This article gives a detailed account of how Node JS fundamentally works.

What is Node JS?

Node JS official documentation

Despite a common misconception, Node JS is not a programming language, a framework, or a library. It’s also not another name for JavaScript. According to the official documentation over here:

Node JS is an open-source, cross-platform JavaScript runtime environment.

When I first started learning about Node JS, this definition did not make much sense to me, so let me break it down for you:

  1. Open source

This means that the source code, its associated components, and documentation is freely and readily available for anyone.

2. Cross platform

Node JS itself is designed to work across different operating systems, without modifications to your source code.

3. Runtime environment

All programming languages require a compiler or an interpreter to interpret and execute your source code. For JavaScript, this compiler is referred to as the “JavaScript Engine”. This Engine converts your source code into machine code so that it can understand and execute it.

All browsers have an in-built JavaScript Engine that is capable of interpreting and executing your typical JS code to make your website interactive. Perhaps the most popular JavaScript Engine is Google’s V8 Engine, which powers Google Chrome and, you guessed it, Node JS!

Think of a runtime environment as a software environment that encompasses the JavaScript Engine along with additional functionalities. Browsers and servers could have different runtime environments despite using the same JavaScript Engine.

Chrome’s runtime environment includes the V8 Engine and the DOM API that allows JavaScript code to manipulate and interact with a web page. On the server side, Node JS’ runtime environment also includes the V8 Engine and APIs for file system access and network communication, among other functionalities.

To summarise, Node JS is merely a freely available, platform-independent software environment that interprets and executes your JS code on the server. It is not a programming language or a framework.

How does Node JS work?

To fully grasp how the Node JS runtime environment works, it is important to understand the following three main components:

  1. V8 Engine
  2. Event Loop
  3. Libuv

The V8 Engine

As previously mentioned, this JavaScript Engine interprets and executes JS code on both Chrome and Node JS runtime environments. Here is a high-level overview of the V8 Engine:

Simplified overview of the V8 Engine

These building blocks of the V8 Engine manage the execution of the JavaScript code:

Memory heap: The memory heap is a space in memory that has been reserved for use by the Node JS runtime for memory allocation. Whenever a variable is created that holds, for example, an object, a number, or a string, the engine saves the variable and its value in the memory heap. Here is a high level overview of what happens when you create a variable called valueand assign it a value of 123.

let value = 123; 

While executing JS code, the V8 Engine allocates separate memory slots to store value and 123. Then, it associates 123 with value. Hence, the variable value now points to the memory location where 123 is stored.

Whenever you use the variable value in your code, the JavaScript engine accesses the memory location associated with it and retrieves the value 123.

Call stack: This stack data structure keeps a track of what function is currently being executed and the next functions to be executed. You can read more about the call stack over here, but we also outline a very simple example below.

JS is a single-threaded programming language, which means that JS code is executed in a single sequence, one operation at a time. Hence, the V8 Engine only has one call stack which follows the Last-in-First-Out (“LIFO”) principle as the stack data structure. In a “LIFO” data structure, as a “stack”, the last element added to the stack is the first one that is removed. You can read more about the “LIFO” principle and stacks here. Let’s say you have some JS code as below:

function add(a,b) {
return a + b;
}

function printSum(a, b) {
const c = add(a, b);
console.log(c);
}

printSum(3, 4);

At the beginning, the call stack is empty. Once the Engine starts executing the above code, this is how the call stack looks sequentially:

Call stack for the above source code

Each entry into the call stack is referred to as the “stack frame”.

  1. printSum is a function call, so it is pushed on to the stack.
  2. Inside printSum we call the add function, which is also pushed onto the stack.
  3. The add function finishes executing and the result of the function is assigned to variable c with the variable and the value stored in the memory heap. Hence, the add function is now removed from the stack.
  4. Finally, the printSum function is executed and also removed from the stack.

From this, we can see that the function that is on top of the stack is executed first, i.e. the call stack follows the “LIFO” principle just like the stack data structure.

Since the V8 Engine has only a single call stack, running code on a single thread could cause “blocking” when function execution is slow. For instance, your source code could look something like this:

function imageTransformation() {
someCodeToTransformImage(); // Takes 1 minute
}

function getData() {
someCodeToGetData(); // Takes 2 minutes
}

imageTransformation();
getData();

For the above source code, this is how the call stack looks sequentially (from left to right):

Call stack for the above source code

Here are some important points to highlight:

  1. The imageTransformation and getData functions are executed synchronously — i.e. one after another.
  2. This implies that the getData function has to wait for one minute for the imageTransformation function to finish executing before it can execute itself.
  3. In other words, the getData function is “blocked” by the imageTransformation function for one minute.
  4. Hence, executing synchronous functions on a single thread is limiting when the functions are slow. This blocks other functions from executing, which is far from ideal.

So how do we solve this problem? This would be an ideal point to introduce the Event Loop and talk about asynchronous functions.

The Event Loop

Here is some source code that includes an asynchronous function setTimeout . You can read more about the setTimeout function here:

console.log("Hello");

setTimeout(function cb () {
console.log("Done");
}, 5000);

console.log("World");

The output of this source code on the console is show below where “Hello” and “World” are logged to the console first. “Done” is then logged five seconds later:

Hello
World
Done

Important: For the setTimeout function, we specify a delay of x milliseconds. x is not a guaranteed time to execution, but it is the minimum time the JS runtime will take to execute the callback function. For the above source code, however, console.log("Done"); gets executed after five seconds.

Before the source code is executed, the call stack is empty. This is how the call stack looks for the above source code through different steps of execution:

Call stack for the above source code
  1. console.log("Hello"); is added to the call stack and is popped off the stack and executed. Now the call stack is empty again.
  2. The setTimeout function is added to the call stack, but it doesn’t start executing straight away. The function just disappears from the call stack.
  3. As explained in step one, the same applies for console.log("World");
  4. Five seconds after the above, the callback function is added onto the call stack. Then, the content of the callback function, i.e. console.log("Done"); appears on the call stack and is executed. Finally, the callback function is popped off the call stack.

Going back to step two, this is where the concept of Event Loop comes in. The JS V8 Engine itself is capable of executing only one thing at a time. However, the Node JS runtime, which includes the V8 Engine, gives us other C++ APIs and capabilities that are effectively threads which can’t be directly accessed.

Node JS runtime environment

For the browser runtime environment, the browser provides the Web APIs instead of the C++ APIs, which includes the DOM as shown below.

Browser Runtime environment

This is how the call stack and callback queue work for the above source code. In the video below, note that Web APIs is just for illustration purposes. Node JS calls the C++ APIs and does not have any Web APIs.

Call stack, callback queue for the above source code
  1. The setTimeoutfunction is added to the call stack. For the setTimeoutfunction, we pass a callback function cb which logs “Done” to the console and a delay of five seconds. The setTimeout function does not live within the core V8 Engine. The exact API Node JS calls to access the setTimeout function depends on your operating system (OS). For Unix-based systems, Node JS uses the libuv library. The libuv library utilises your OS’ timer mechanisms to start a five second timer.
  2. Since the call to the setTimeout function is now complete, it is popped off the stack.
  3. After the C++ API has completed executing the callback function, it is added to the callback queue which is shown by cb().
  4. This is where the job of the Event Loop is significant. The Event Loop (indicated by the orange 360 degree arrow in the above picture) looks at the stack and the callback queue. It is only if the stack is empty that it will take the first thing on the callback queue and push it onto the stack.
  5. In this scenario, the stack is empty and the callback queue consists of the callback function cb . Hence, the Event Loop pushes the callback function cb from the queue onto the stack.
  6. After the callback function cb is pushed onto the call stack, it gets executed and “Done” is logged to the console. Then, cb is popped off the stack.

Now, let’s look at what happens in a scenario where the call stack is not empty and there is a function waiting to be executed on the callback queue.

console.log("Hello");

setTimeout(function cb () {
console.log("Done");
}, 0);

console.log("World");

Over here, the callback function, cb is executed with a delay of 0 seconds. You might expect the callback to be executed immediately since the timeout has a delay of 0s. However, this isn’t quite how it works.

Call stack, callback queue for the above source code

There are important points to highlight from the above visualisation:

  1. Even though the callback queue contains the function cb , it is not pushed onto the call stack by the Event Loop.
  2. This is because console.log("World"); is on the call stack.
  3. Once console.log("World"); is popped off the stack, the stack is empty. It’s only when the stack is empty that cb is pushed onto the stack from the callback queue by the Event Loop.

The setTimeout function with a delay of 0 seconds is a way to delay the execution of your code until other synchronous functions have finished executing.

Libuv

Libuv official documentation

According to the official documentation here:

Libuv is a multi-platform library with a focus on asynchronous Input/Output (I/O) operations.

In other words, this crucial cross-platform component of Node JS is capable of performing I/O operations within your operating system. Libuv is capable of running on any operating system and abstracts the underlying operating system’s I/O functionality.

As previously mentioned, although JS is a single-threaded language, the APIs allow multiple thread pools to be used. For instance, the underlying implementation of Libuv is designed to use multiple threads of the operating system. Hence, the Libuv API, written in C, is capable of executing multiple I/O operations at the same time. Although these threads cannot be directly accessed via JS source code, you can make API calls to the library which can utilise multiple threads of the operating system to execute multiple functions concurrently.

Let’s say your source code contains some asynchronous I/O operations that interact with the computer’s file system. Whilst parsing your code, the Engine will make an API call to the Libuv library to execute those asynchronous I/O operations.

Closing words

To conclude, Node JS is not a programming language or a framework. Rather, it is simply a runtime environment that allows your JavaScript source code to be executed on the server side.

The Node JS runtime environment is comprised of three main components: The V8 Engine (which also powers Google Chrome), Event Loop, and Libuv:

  • The V8 Engine consists of the interpreter which can execute your JS code.
  • The Event Loop assists in the execution of asynchronous functions by continuously checking the call stack and the callback queue.
  • Libuv is an API that is built on C. Your JS source code can make API calls to this library to execute asynchronous I/O operations to interact with the computer’s file system

Additional resources

To learn more about developing server-side applications using Node JS, there are plenty of excellent resources around. Here are some of my personal favourites:

Got a question?

Please get in touch to speak to a member of our team at Credera.

--

--