Event Loop in NodeJs
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:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");
What prints?
Most beginners instinctively say:
A
B
C
But Node prints:
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:
- Timers
- Pending callbacks
- Poll
- Check
- Close callbacks
- Microtasks (special case)
Developers don’t think in phases. They think in patterns:
| Action | Queue | Example |
|---|---|---|
| Timers | Timer queue | setTimeout, setInterval |
| I/O | Poll queue | network, fs, DNS |
| Check | Check queue | setImmediate |
| Microtasks | Microtask queue | Promises, queueMicrotask |
Here’s a fun one:
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:
promise
timeout
Output:
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:
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:
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
async function main() {
console.log("start");
await new Promise(r => setTimeout(r, 1000));
console.log("end");
}
main();
Outputs:
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.
