Sitemap
somo_p

somo_p

Software Engineer

JavaScript Event Loop — Callback Queue, Microtask Queue & Starvation

4 min readMar 7, 2026

--

Press enter or click to view image in full size

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:

  1. It is pushed onto the stack
  2. It executes
  3. 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)
End

The 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:

  • setTimeout
  • fetch
  • DOM events
  • geolocation
  • localStorage
Press enter or click to view image in full size

When JS calls them, they run outside the call stack.

Example:

setTimeout(() => {
console.log("Timer done");
}, 2000);

Process:

  1. setTimeout goes to Web API
  2. Timer runs there
  3. 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:

  • setTimeout
  • setInterval
  • DOM events
  • network callbacks

Example:

console.log("Start");

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

console.log("End");

Output:

Start
End
Timer

Even 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:

  1. Take a task from the queue
  2. Push it into the call stack
  3. 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()
  • MutationObserver
  • process.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
Timeout

Execution flow:

Call Stack → empty
Event Loop checks Microtask Queue first
Then Macro Task Queue

8. Full Execution Order

The complete order is:

1. Synchronous code
2. Microtask queue
3. Macro task queue

Visual 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 runs

This is called Microtask Starvation.

Why It Happens

Event Loop rule:

Process ALL microtasks
before running next macrotask

If 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
B

Reason:

sync → microtask → macrotask

11. 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 queue

Chef rule:

Finish ALL VIP orders first
then normal orders

If VIP orders keep coming → normal orders starve.

13. Final Summary

Key ideas:

  1. JavaScript is single threaded
  2. Call stack executes synchronous code
  3. Web APIs handle async work
  4. Completed callbacks go to queues
  5. Event loop moves tasks to stack
  6. Microtasks run before macrotasks
  7. Infinite microtasks can cause starvation

--

--