Event Loop in NodeJs

Martin Oputa
Node.jsEventsArchitecture

If you read the previous article about Node.js being event-driven, you probably left with one lurking question:

Okay, but who listens for those events? Who triggers the callbacks? Who decides when timers, I/O, or promises should run?

The answer is the event loop, a choreography system that keeps Node responsive without spawning hundreds of threads. This article is the missing mental link between “Node is event-driven” and “Node is magically non-blocking.” Once you understand the event loop, timers, I/O, async/await, promises, and network servers all snap into focus.

A Coffee Shop With One Barista

Imagine a tiny coffee shop with one barista. Customers place orders. The barista puts these orders into a queue (espresso, cappuccino, pastry, etc.), works through them one at a time, and rings a tiny bell when an order is done.

But here’s the trick: the barista doesn’t wait there staring at the espresso machine — they start prepping the next drink, wiping a counter, or grabbing a pastry. When the espresso machine beeps, the barista handles the finished drink.

Node works the same way:

  • calling async functions = placing an order
  • callbacks/promises = bell ringing
  • event loop = barista
  • libuv threadpool / kernel I/O = espresso machine

So What Is the Event Loop?

Technically:

The event loop is a mechanism that schedules and dispatches work from queues based on phases.

Practically:

It decides when your setTimeout, network requests, filesystem I/O, and promises execute.

If you understand nothing else, remember this rule:

The event loop never blocks on I/O.

Which is why Node can serve thousands of connections with one thread.

A Simple Example (But Sneaky)

Look at this snippet:

js
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");

What prints?

Most beginners instinctively say:

text
A
B
C

But Node prints:

text
A
C
B

Why?

  • setTimeout puts the callback into the timer queue
  • It doesn’t execute immediately
  • “C” runs because JavaScript execution is synchronous
  • After that tick finishes, the event loop checks the timer queue

But Why Doesn't setTimeout(fn, 0) Run Immediately?

Because queueing is never immediate, there’s no teleportation. Even a 0ms timeout still means:

“Run after the current execution stack is empty.”

This subtle guarantee is what allows async code to not interrupt synchronous execution.

The Six Phases (But Simplified for Humans)

Formally, Node describes the loop as having phases:

  1. Timers
  2. Pending callbacks
  3. Poll
  4. Check
  5. Close callbacks
  6. Microtasks (special case)

Developers don’t think in phases. They think in patterns:

ActionQueueExample
TimersTimer queuesetTimeout, setInterval
I/OPoll queuenetwork, fs, DNS
CheckCheck queuesetImmediate
MicrotasksMicrotask queuePromises, queueMicrotask

Here’s a fun one:

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

Which runs first?

Answer: It depends.

In I/O scenarios, setImmediate often wins. Node keeps life interesting.

Microtasks: The VIP Queue

If the event loop is a barista, microtasks are like VIP customers with “cut the line” privileges. Promises belong here.

Example:

text
promise
timeout

Output:

text
promise
timeout

Even though both are async, microtasks run after the current JS execution but before the next event loop phase.

The Misunderstood “Blocking Code” Problem

Blocking code in Node doesn’t mean async is broken. It means you put the barista on hold.

Example of blocking:

js
const fs = require("fs");

const data = fs.readFileSync("data.json"); // BLOCKS
console.log("done!");

During that read:

  • no new requests can be processed
  • no timers can fire
  • nothing else happens

The barista stood staring at the espresso machine waiting.

Non-blocking version:

js
fs.readFile("data.json", () => {
  console.log("done!");
});

Look, the barista can keep serving drinks

What About async / await?

await just wraps promises into readable syntax. Under the hood:

  • still microtasks
  • still async
  • still event loop scheduled
js
async function main() {
  console.log("start");
  await new Promise(r => setTimeout(r, 1000));
  console.log("end");
}
main();

Outputs:

js
start
end (after 1s)

Even though it looks blocking, the barista keeps working.

Where the Event Loop Shines (Real-World Cases)

Node is tailor-made for:

  • web servers
  • network apps
  • proxies & gateways
  • websocket & realtime apps
  • IoT and streaming
  • queues & schedulers
  • pipelines

Why?

Because I/O latency is everywhere and latency is free parallelism.

Where the Event Loop Struggles

Node struggles when the work is:

  • CPU-bound
  • data crunching
  • crypto or compression
  • ML or image processing

These pin the barista in place.

Solutions include:

  • worker threads
  • clustering
  • offloading to another service
  • native modules

Final Thoughts: The Event Loop Is the Magic

Everything you love about Node:

  • async I/O
  • scalability
  • lightweight processes
  • websocket performance
  • event-driven architecture

…comes from the event loop. Once you internalize that Node has: One barista + many machines + a bell, the platform’s design finally makes sense. And here's the real payoff: if you understand the event loop, you understand how to make Node fast.