JavaScript Event Loop — Callback Queue, Microtask Queue & Starvation
1. Introduction
JavaScript is single-threaded, meaning it can execute only one task at a time in the main thread. Yet browsers and Node.js can still handle asynchronous operations like API calls, timers, and promises.
This is possible because of the Event Loop architecture.
The system involves:
- Call Stack
- Web APIs / Node APIs
- Callback Queue (Macro Task Queue)
- Microtask Queue
- Event Loop
These components together allow non-blocking asynchronous execution.
2. Call Stack
Concept:
The Call Stack is where JavaScript executes functions.
It follows LIFO (Last In First Out).
When a function runs:
- It is pushed onto the stack
- It executes
- It is removed when finished
function one() {
two();
}
function two() {
console.log("Hello");
}
one();Execution order:
Call Stack
-----------
| console.log() |
| two() |
| one() |
| global() |Then each function finishes and pops from the stack.
3. Problem With Single Thread
Imagine this code:
function heavyTask() {
for(let i = 0; i < 1000000000; i++) {}
}
console.log("Start");
heavyTask();
console.log("End");Output:
Start
(wait for loop)
EndThe browser freezes.
Because the call stack is blocked.
To solve this problem JavaScript uses Async APIs + Event Loop.
4. Web APIs / Browser APIs
Browsers provide background capabilities called Web APIs.
Examples:
setTimeoutfetchDOM eventsgeolocationlocalStorage
When JS calls them, they run outside the call stack.
Example:
setTimeout(() => {
console.log("Timer done");
}, 2000);Process:
setTimeoutgoes to Web API- Timer runs there
- After completion → callback goes to queue
5. Callback Queue (Macro Task Queue)
The Callback Queue (also called Task Queue / Macro Task Queue) stores callbacks from asynchronous APIs.
Examples:
setTimeoutsetInterval- DOM events
- network callbacks
Example:
console.log("Start");
setTimeout(() => {
console.log("Timer");
}, 0);
console.log("End");Output:
Start
End
TimerEven with 0ms, the callback waits for the event loop.
Why?
Because the call stack must first be empty.
6. Event Loop
The Event Loop constantly checks:
Is the call stack empty?If yes:
- Take a task from the queue
- Push it into the call stack
- Execute it
Simplified algorithm:
while(true){
if(callStack.empty){
moveTaskFromQueueToStack()
}
}7. Microtask Queue (Job Queue)
JavaScript also has a higher-priority queue called the Microtask Queue.
Get somo_p’s stories in your inbox
Join Medium for free to get updates from this writer.
Examples:
Promise.then()queueMicrotask()MutationObserverprocess.nextTick(Node)
Important rule:
Microtasks run before Macrotasks.
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");Output:
Start
End
Promise
TimeoutExecution flow:
Call Stack → empty
Event Loop checks Microtask Queue first
Then Macro Task Queue8. Full Execution Order
The complete order is:
1. Synchronous code
2. Microtask queue
3. Macro task queueVisual model:
┌───────────────┐
│ Call Stack │
└──────┬────────┘
│
Event Loop
│
┌────────────┴────────────┐
│ │
Microtask Queue Callback Queue
(Promises) (setTimeout)9. Starvation Problem
Now comes the interesting part.
What is Starvation?
Starvation happens when microtasks keep getting added continuously, preventing macrotasks from ever executing.
Example:
function loop() {
Promise.resolve().then(loop);
}
loop();
setTimeout(() => {
console.log("Timeout executed");
}, 0);What happens?
Microtask added
Microtask executed
Microtask added
Microtask executed
...Since microtasks are always processed first, the event loop never reaches the macrotask queue.
Result:
setTimeout never runsThis is called Microtask Starvation.
Why It Happens
Event Loop rule:
Process ALL microtasks
before running next macrotaskIf microtasks keep generating more microtasks → infinite cycle.
10. Practical Interview Trick Question
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve()
.then(() => console.log("C"))
.then(() => console.log("D"));
console.log("E");Output
A
E
C
D
BReason:
sync → microtask → macrotask11. Real-World Use Cases
Microtasks
Used for:
- Promise resolution
- Async/await continuation
- DOM mutation observation
Macrotasks
Used for:
- Timers
- Network events
- UI events
- message channels
12. Simple Mental Model
Think of it like a restaurant.
Chef = Call Stack
Orders:
VIP orders → Microtask queue
Normal orders → Callback queue
VIP orders → Microtask queue
Normal orders → Callback queueChef rule:
Finish ALL VIP orders first
then normal ordersIf VIP orders keep coming → normal orders starve.
13. Final Summary
Key ideas:
- JavaScript is single threaded
- Call stack executes synchronous code
- Web APIs handle async work
- Completed callbacks go to queues
- Event loop moves tasks to stack
- Microtasks run before macrotasks
- Infinite microtasks can cause starvation








