Skip to main content

Command Palette

Search for a command to run...

Node.js Event Loop Explained Clearly

A beginner-friendly guide to understanding how Node.js stays fast while handling slow work in the background

Updated
7 min read
Node.js Event Loop Explained Clearly
P
IT graduate 2024. Learning software and web development in public. Writing about bugs, fixes, small projects, useful tools, and lessons from building things step by step.

Introduction

Whenever someone says Node.js is single-threaded, one doubt comes up almost immediately:

then how does it handle so many requests at the same time?

At first, this sounds a little confusing.

Because we often assume single-threaded means slow or limited.

But the real power of Node.js is hidden in its Event Loop model.

This blog exists to clear up that confusion.

We will look at:

  • what the Event Loop is

  • how Node handles slow tasks

  • why setTimeout, Promises, process.nextTick, and I/O behave differently

  • why this topic matters so much in real backend development

The goal is simple:

not to memorize a definition, but to understand the system


Why This Topic Exists

JavaScript normally executes line by line.

console.log("Start");
console.log("End");

Output:

Start
End

This is simple synchronous behavior.

But in the backend world, there is not just fast synchronous code.

A server has to handle:

  • file reading

  • database queries

  • network requests

  • timers

  • external services

If JavaScript waited for every slow operation, the whole server would get blocked.

This is where the Event Loop starts to matter.


Big Picture Flow

First, let us look at the high-level picture:

Request arrives
-> Node delegates the slow task
-> the main thread stays free
-> the task completes
-> the callback becomes ready
-> the Event Loop executes it

That means Node's main idea is not to do every task by itself.

Its real idea is:

hand off the work that needs waiting, and run the work that is ready


A Simple Restaurant Analogy

Imagine a restaurant.

Restaurant Role Node.js Equivalent
Front manager Main JavaScript thread
Kitchen staff libuv workers / system handlers
Orders Tasks
Ready food serving Event Loop execution

The manager does not cook every dish personally.

The manager:

  • takes the order

  • gives the work to the kitchen

  • tracks which orders are ready

  • and gets them served

Node works in a similar way.

The main thread is not supposed to get stuck on every slow task.

It hands off slow work to the right system and keeps executing ready callbacks.


What the Event Loop Actually Does

In simple words:

the Event Loop is a coordination system that executes ready callbacks at the right time

Its job is not to do all the asynchronous work by itself.

Its job is more about:

  • checking which work is ready

  • deciding which queue should be looked at first

  • deciding which callback runs in which phase

That is why the order of async code can sometimes feel surprising.


Step-by-Step: What Happens Internally

Step 1: Synchronous Code Runs First

First, normal synchronous code runs.

Any line that can run immediately gets executed right away.


Step 2: Slow Work Gets Delegated

When Node gets a slow task, it does not hold onto it in a blocking way.

Instead, it hands the task off to:

  • the operating system

  • or the libuv thread pool

Examples:

  • file system work

  • some DNS work

  • crypto tasks


Step 3: Task Completes in the Background

The background system completes its work.

When the result is ready, the callback becomes available to execute.

But the callback does not run immediately every time.

It runs according to Event Loop scheduling.


Step 4: Event Loop Picks Ready Work

Now the Event Loop checks:

which callback should run now?

Then the callback runs according to the correct phase and priority.


Event Loop Phases

The Node.js Event Loop is not a simple single-queue system.

It has phases.

At a high level, the phases are:

1. Timers
2. Pending Callbacks
3. Poll
4. Check
5. Close Callbacks

Important ones:

Phase Main Work
Timers setTimeout, setInterval
Poll I/O related callbacks
Check setImmediate

These phases often decide execution order in many cases.

nodejs-event-loop-explained-clearly


Microtasks: The Hidden Priority Layer

Above the Event Loop phases, there is one more important concept:

microtasks

In the Node.js context, two names are especially useful to remember at the beginner level:

  • process.nextTick

  • Promise callbacks like .then()

At a very high level, the priority often looks like this:

process.nextTick
-> Promise microtasks
-> Event Loop phases

That is why sometimes a Promise or nextTick appears to run before a timer.


Example: process.nextTick vs Promise

console.log("Start");

process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));

console.log("End");

Output:

Start
End
nextTick
promise

Here, process.nextTick came first.

Reason:

Node.js processes the nextTick queue even before Promise microtasks.


Example: Promise vs setTimeout

console.log("A");

setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));

console.log("D");

Output:

A
D
C
B

Here:

  • the Promise callback is a microtask

  • setTimeout goes into the timer phase

That is why the Promise executed first.


Example: setTimeout vs setImmediate

This topic is a little context-dependent.

Case 1: Normal Flow

setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));

In normal situations, you can commonly see:

timeout
immediate

But do not treat this like a fixed rule.

Context matters.


Case 2: Inside I/O

const fs = require("fs");

fs.readFile("file.txt", () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
});

Typical output:

immediate
timeout

Reason:

After I/O, the flow moves from the Poll phase toward the Check phase, and setImmediate runs in the Check phase.

That is why in this context it can appear first.


A Real Backend Example

const fs = require("fs");

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

console.log("Server running");

Output:

Server running
File content loaded

Here, Node did not block while waiting for the file read.

It delegated the file task and kept running the next synchronous code.

This non-blocking behavior is what keeps backend apps responsive.


Why This Matters in Real Development

Understanding the Event Loop is not just for interviews.

It helps in real work with:

  • understanding async bugs

  • reasoning about unexpected log order

  • predicting the behavior of timers and Promises

  • improving backend performance thinking

  • identifying blocking code

Once the Event Loop mental model becomes clear, Node.js stops feeling random.


Summary

  • Node.js is single-threaded, but it does not follow a blocking model

  • slow tasks can be delegated to background systems

  • the Event Loop executes ready callbacks

  • phases affect execution order

  • process.nextTick and Promise callbacks can run before normal timers

  • the order of setImmediate vs setTimeout can change depending on the context


Final Thought

Once you understand the Node.js Event Loop, the phrase "single-threaded" starts to feel much less scary or confusing.

At that point, you are not just looking at syntax anymore.

You are seeing the flow of the system.

And in backend learning, that shift is one of the most valuable ones.


Continue Reading