The Event Loop — JavaScript’s Biggest Lie (It’s Single-Threaded… Sort Of)
If you’ve ever been in a JavaScript interview, you’ve heard this line: “JavaScript is single-threaded.”
You nod. The interviewer nods. Everyone pretends they fully understand what that means.
But here’s the thing — if JavaScript can only do one thing at a time, how does it handle thousands of users, run timers, fetch APIs, and read files all at once without freezing?
The answer is the Event Loop. And once you understand it, JavaScript stops being confusing and starts being beautiful.
Let’s Start With a Kitchen
Imagine you’re a chef. Alone. One stove, one counter, one pair of hands. That’s JavaScript — a single-threaded runtime. You can only chop one vegetable at a time.
That’s the Event Loop in a nutshell. JavaScript doesn’t wait. It delegates, moves on, and comes back when things are ready.
The Actual Architecture
Here’s what’s happening under the hood:
↓
Is it synchronous? → Execute immediately
Is it async? → Send to Web APIs / libuv
↓
When done → Push callback to the Queue
↓
Event Loop checks: Is the Call Stack empty?
Yes → Pick from Queue → Push to Call Stack → Execute
No → Wait
Three key players:
1. Call Stack
Where your code actually runs, one function at a time
2. Web APIs / libuv
Where async tasks (timers, HTTP requests) go to wait
3. Callback Queue
Where completed tasks line up, waiting for the stack to be free
The Event Loop is just a bouncer. It constantly asks: “Is the Call Stack empty? No? Wait. Yes? Let the next callback in.”
The Code That Breaks People’s Brains
setTimeout(() => {
console.log(“Second”);
}, 0);
console.log(“Third”);
Output:
First
Third
Second
Wait — setTimeout is set to 0 milliseconds. Zero! Why doesn’t “Second” print second?
Because setTimeout — even with 0ms — is an async operation. It gets sent to the Web API, then to the Callback Queue. The Event Loop won’t pick it up until the Call Stack is completely empty.
Microtasks vs Macrotasks (The VIP Queue)
Not all async callbacks are equal. There are two queues:
Microtask Queue (VIP): Promises, process.nextTick(), MutationObserver
Macrotask Queue (Regular): setTimeout, setInterval, setImmediate, I/O callbacks
The Event Loop always empties the Microtask Queue first before touching any Macrotask.
Promise.resolve().then(() => console.log(“Promise”));
console.log(“Sync”);
Output:
Sync
Promise
Timeout
The Promise (microtask) jumps ahead of setTimeout (macrotask). Every. Single. Time. This isn’t a race condition — it’s by design.
Why This Matters in Real Life
Understanding the Event Loop is the difference between a Node.js server that handles 10,000 concurrent connections and one that freezes because you put a heavy for loop in the main thread.
If you block the Call Stack with synchronous heavy computation, nothing else runs. No API responses. No timers. No database callbacks. Everything waits.
That’s why you’ll hear people say “Don’t block the Event Loop.” It’s not a suggestion. It’s survival advice.
The Number That Blows My Mind
Node.js, powered by the Event Loop, can handle roughly 1 million concurrent connections on a single thread with proper async code. Apache starts struggling at around 10,000. That’s a 100x difference — not from more hardware, but from smarter architecture.
