Skip to main content

Command Palette

Search for a command to run...

The Node.js Event Loop Explained

Updated
4 min readView as Markdown

What is the Event Loop?

The Event Loop is a system inside Node.js that continuously checks:

  • Is any task ready to execute?

  • Is the call stack empty?

  • Are there pending async operations?

If yes, it moves the completed tasks into execution.

In simple words:

The event loop helps Node.js perform non-blocking operations even though JavaScript uses a single thread.

Why Node.js Needs an Event Loop

JavaScript was originally designed for browsers and used a single-threaded execution model.

That means:

  • One task executes at a time

  • Long-running tasks can block everything else

Imagine this:

console.log("Start");

while(true) {
  // infinite loop
}

console.log("End");

The "End" statement will never run because the main thread is blocked.

Now think about server applications:

  • Reading files

  • Calling databases

  • Handling network requests

  • Waiting for APIs

These operations can take time.

If Node.js waited for each task to finish before moving forward, the server would become very slow.

So Node.js introduced:

  • Async operations

  • Event loop

  • Callback handling

This allows Node.js to continue handling other users while waiting for slow operations to complete.

Understanding the Call Stack

The Call Stack is where JavaScript executes functions.

JavaScript executes code line by line using this stack.

Example:

function greet() {
  console.log("Hello");
}

greet();

Execution flow:

  1. greet() goes into stack

  2. console.log() executes

  3. Function removed from stack

The stack only handles synchronous code directly.

Task Queue vs Call Stack

Node.js uses two important concepts:

Component Purpose
Call Stack Executes functions
Task Queue Stores completed async callbacks

How They Work Together

Example:

console.log("Start");

setTimeout(() => {
  console.log("Timer Done");
}, 2000);

console.log("End");

Output:

Start
End
Timer Done

Step-by-step Flow

"Start" executes

Added to call stack and printed.

setTimeout() starts

Node.js sends the timer operation to browser/Node APIs.

The timer runs outside the main JavaScript thread.

"End" executes

Since setTimeout() is asynchronous, JavaScript does not wait.

  1. Timer finishes

The callback:

() => { 
    console.log("Timer Done"); 
}

moves into the task queue.

Event Loop checks the stack

If the call stack is empty:

  • Event loop pushes callback into stack

  • Callback executes

Then output becomes:

Timer Done

How Async Operations Are Handled

Node.js uses system-level APIs and internal worker mechanisms to handle async tasks outside the main thread.

Examples of async operations:

  • File system operations

  • Database queries

  • API requests

  • Timers

  • Network communication

Example:

const fs = require("fs");

console.log("Start");

fs.readFile("data.txt", "utf8", (err, data) => {
  console.log(data);
});

console.log("End");

Output:

Start
End
[file content]

What Happened Internally?

Step 1

readFile() is delegated to Node.js system APIs.

Step 2

Node.js continues executing remaining code.

Step 3

When file reading completes:

  • Callback enters task queue

  • Event loop waits for empty call stack

  • Callback executes

This is why Node.js remains fast and responsive.

Timers vs I/O Callbacks

The event loop processes different kinds of tasks in phases.

At a high level:

Type Example
Timers setTimeout(), setInterval()
I/O Callbacks File reads, database responses, network events

Timers Phase

Handles callbacks scheduled using:

setTimeout()
setInterval()

Example:

setTimeout(() => {
  console.log("Runs after 2 seconds");
}, 2000);

I/O Callback Phase

Handles completed operations like:

  • File reading

  • HTTP responses

  • Database operations

Example:

fs.readFile()

These callbacks execute when the operation completes.

Role of Event Loop in Scalability

The event loop is one of the biggest reasons Node.js became popular for backend development.

Because Node.js does not block while waiting for operations:

  • One server can handle many users

  • Memory usage remains lower

  • Response handling becomes efficient

This is especially useful for:

  • Chat applications

  • Streaming services

  • Real-time apps

  • APIs

  • Multiplayer games

Blocking vs Non-Blocking Example

Blocking

const data = fs.readFileSync("data.txt", "utf8");
console.log(data);

Execution stops until file reading finishes.

Non-Blocking

fs.readFile("data.txt", "utf8", (err, data) => {
  console.log(data);
});

Node.js continues executing other tasks.

This improves concurrency and scalability.