The Node.js Event Loop Explained

Be sure to check out Why Node.js? and The Node.js Event Loop before taking a deep dive...

JavaScript is single threaded. It has a single call stack and a single memory heap. It's synchronous meaning code runs in order...

console.log(1)
console.log(2)
console.log(3)
//logs
1
2
3

This makes sense but can be problematic if something takes awhile to run...

let data = fs.readFileSync('MyFile.csv')
processData(data)
someOtherFunction()

It may take awhile to read the file depending on it's size. Since javaScript is synchronous, we have to wait for readFileSync to complete before the rest can execute.

Callbacks to the rescue

Waiting for blocking operations (talking to the file system, making HTTP requests, reading from a DB) can take time. We don't want the rest of our code to wait around for these synchronous operations to complete.

Using callbacks, we can make synchronous (blocking) operations asynchronous (non-blocking)...

fs.readFile('MyFile.csv', (err, data) => {
  if(err) throw err
  processData(data)
})
someOtherFunction()

Notice how the second argument in the fs.readFile() is a callback function. This function runs when the file read completes and the data is available. If there are any errors reading the file, an exception will be thrown.

This is an asynchronous way to handle blocking I/O. The script can move on to running someOtherFunction() without waiting for fs.readFile() to finish running. The processData() function runs only after the data is available by way of the callback function.

Callbacks and the Node.js Event Loop

Callbacks aren't something new with Node. They represent a popular design pattern used with traditional JavaScript for many years.

By themselves, callbacks are a simple way to say "hey when this is done, then do this". Node simply leverages callbacks on certain methods to register events on the event loop...

So what is the Node.js Event Loop?

The event loop is not specific to Node.

JavaScript runs inside an environment...

JavaScript code always requires an environment to run in. Sometimes this is your browser. Sometimes it's Node. This environment includes both the engine for your JavaScript code to run (V8) and the orchestrator between your JavaScript application and lower level interfaces (http, file system, DB).

Lower level interfaces???

The JavaScript engine itself is not equipped to handle lower level i/o operations like http calls, file reading/writing, DNS, etc. Even things like setTimeout() and setInterval() aren't defined in the V8 source code.

Instead, JavaScript interfaces with additional threads to handle these blocking operations. For browsers, these are Web APIs. For Node, its C++ APIs.

The event loop orchestrates the JavaScript code you write with these async interfaces. For example...:

console.log("start")
fs.readFile("Myfile.csv", (err, data) => {
  if(err) throw err
  console.log("file read")
})
console.log("end")

will print...


start
end
file read

The first line is a synchronous call that gets immediately added to the call stack. It gets executed first and we see the first line:

start

The second line is a blocking i/o call to the file system. This process gets scheduled on a separate thread and the code continues synchronously printing..

end

Some time later, the file read operation completes. The callback for the operation is added to the callback queue...

The event loop sees the call stack is empty and adds / executes the callback from the callback queue printing...

file read

The event loop is a single threaded process which continuously adds callbacks from a callback queue to the main call stack of your JavaScript application. This allows you to write non-blocking JavaScript code across different environments.

The Node.js Event Loop

The event loop exists in any environment JavaScript runs. For web it's the browser, for Node it's...Node.

Remember that Node is a runtime. It runs the JavaScript you write on the V8 engine and implements the event loop that coordinates lower level non-blocking i/o with your application.

How does the Node.js Event Loop Work?

The Event Loop and libuv

libuv is a lower level C library that implements the event loop. When you talk about the Node event loop you're really talking about the libuv event loop.

This loop runs on a single thread. It's basically a while loop that listens to file descriptors (sockets) for new connections and read/write operations.

Each iteration of the loop is called a 'tick'. Each tick of the loop runs through a series of phases:

timer phase

This phase checks for any timers that have reached their duration and adds them to callback queue.

pending callbacks

This phase executes callbacks for system operations like TCP errors.

poll phase

This phase polls for any blocking i/o that's been completed. For example when an http request completes, it notifies the event loop and it's callback is added to the callback queue.

The poll phase synchronously runs the callback functions in the queue. Once the queue is empty, the poll phase either moves to the next phase or check for timers whose threshold has been reached.

If timers are ready, those callbacks are then executed...

check phase

Once the callback queue is empty, the poll phase gives the check phase an opportunity to run...anything called with setImmediate() gets executed here.

close phase

If something like a socket closes unexpectedly, it's callback is run in this phase of the event loop.

Is the Node.js Event Loop Single Threaded?

The libuv implementation of the event loop is single threaded. Using this single thread, libuv can handle multiple TCP connections more efficiently than traditional alternatives like Java (Apache) which allocate one thread per connection.

Node IS NOT single threaded. While the event loop (libuv) is single threaded, a separate pool of worker threads is also utilized when necessary (mostly for file i/o).

While Node isn't single threaded, it ultimately utilizes fewer threads to serve more concurrent requests. This allows Node to scale particularly well as a web server over alternatives. Rather than pay space and time overhead for new threads, Node can focus on serving clients with the coordination of the event loop and non-blocking i/o.

Your thoughts?