Introduction
The callback queue (also called the macrotask queue or task queue) is a waiting area where asynchronous callbacks sit until the call stack is empty. When a timer expires, a network request completes, or a DOM event fires, the browser doesn't immediately run the callback — it places it in the callback queue first.
Understanding the callback queue is essential because it explains:
- Why
setTimeout(fn, 0) doesn't run immediately - Why synchronous code always runs before async callbacks
- How the event loop decides what to run next
- Why Promises run before
setTimeout callbacks
How the Callback Queue Works
┌──────────────────────────────────────────────────────────────────┐
│ JAVASCRIPT RUNTIME OVERVIEW │
│ │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ CALL STACK │ │ WEB APIs │ │
│ │ │ │ setTimeout(cb, 1000) │ │
│ │ main() │─────▶│ fetch(url) │ │
│ │ setTimeout() │ │ addEventListener() │ │
│ │ │ │ setInterval(cb, 500) │ │
│ └────────┬────────┘ └──────────────┬───────────────────┘ │
│ │ │ │
│ │ │ When task completes, │
│ │ │ callback is pushed │
│ │ ▼ │
│ │ ┌──────────────────────────────────┐ │
│ │ │ MICROTASK QUEUE (HIGH PRIORITY) │ │
│ │ │ Promise.then() queueMicrotask │ │
│ │ └──────────────┬───────────────────┘ │
│ │ │ │
│ │ ┌──────────────▼───────────────────┐ │
│ │ │ CALLBACK QUEUE (MACROTASK) │ │
│ │ │ [cb1] → [cb2] → [cb3] → ... │ │
│ │ │ setTimeout setInterval I/O │ │
│ │ └──────────────┬───────────────────┘ │
│ │ │ │
│ │ EVENT LOOP │ │
│ │ ┌──────────┐ │ │
│ └─────────│ Stack │◀────────┘ │
│ │ empty? │ Move ONE callback to stack │
│ └──────────┘ │
└──────────────────────────────────────────────────────────────────┘
The key rules:
- Web APIs complete async work, then push callbacks to the callback queue
- The event loop checks: is the call stack empty? Are microtasks empty?
- Only then does it take ONE callback from the callback queue and push to stack
- After that callback finishes, check microtasks again, then take next macrotask
FIFO Order — First In, First Out
The callback queue is a FIFO structure — callbacks are processed in the order they were added.
// Callbacks added in order: cb1, cb2, cb3
// They execute in the same order: cb1, cb2, cb3
setTimeout(() => console.log("Callback 1 (added first)"), 0);
setTimeout(() => console.log("Callback 2 (added second)"), 0);
setTimeout(() => console.log("Callback 3 (added third)"), 0);
console.log("Synchronous code (runs before ALL callbacks)");
Synchronous code (runs before ALL callbacks)
Callback 1 (added first)
Callback 2 (added second)
Callback 3 (added third)
Callback Queue State (FIFO):
After all setTimeout calls register:
CALLBACK QUEUE: [cb1] → [cb2] → [cb3]
↑ front (executes first)
After cb1 executes:
CALLBACK QUEUE: [cb2] → [cb3]
After cb2 executes:
CALLBACK QUEUE: [cb3]
After cb3 executes:
CALLBACK QUEUE: [] (empty)
Step-by-Step Execution Tracing
console.log("1. Start"); // Sync
setTimeout(() => { // Web API → timer
console.log("4. setTimeout 0ms");
}, 0);
setTimeout(() => { // Web API → timer
console.log("5. setTimeout 100ms");
}, 100);
Promise.resolve()
.then(() => console.log("3. Promise microtask")); // Microtask
console.log("2. End"); // Sync
1. Start
2. End
3. Promise microtask
4. setTimeout 0ms
5. setTimeout 100ms
Detailed Execution Timeline:
TIME ──────────────────────────────────────────────────────▶
0ms:
CALL STACK: [log("1.Start"), setTimeout(0), setTimeout(100), Promise.then, log("2.End")]
→ Executes all sync code
→ setTimeout(0) registered in Web API
→ setTimeout(100) registered in Web API
→ Promise.resolve().then() → callback added to MICROTASK queue immediately
After sync code finishes:
CALL STACK: (empty)
MICROTASK QUEUE: [() => log("3. Promise microtask")]
CALLBACK QUEUE: []
Event loop: "Stack empty, microtasks present!"
→ Runs ALL microtasks → logs "3. Promise microtask"
~0ms: setTimeout(0) fires:
CALLBACK QUEUE: [() => log("4. setTimeout 0ms")]
Event loop: "Stack empty, no microtasks, callback queue has item!"
→ Runs callback → logs "4. setTimeout 0ms"
100ms: setTimeout(100) fires:
CALLBACK QUEUE: [() => log("5. setTimeout 100ms")]
→ Runs callback → logs "5. setTimeout 100ms"
Callback Queue vs Microtask Queue
This is critical for understanding execution order. Microtasks always run before macrotasks (callback queue items).
console.log("SYNC: start");
// MACROTASK (callback queue)
setTimeout(() => console.log("MACROTASK: setTimeout"), 0);
// MICROTASK
Promise.resolve().then(() => {
console.log("MICROTASK: Promise 1");
// Even new microtasks created here run before any macrotask!
Promise.resolve().then(() => console.log("MICROTASK: Promise 2 (nested)"));
});
queueMicrotask(() => console.log("MICROTASK: queueMicrotask"));
console.log("SYNC: end");
SYNC: start
SYNC: end
MICROTASK: Promise 1
MICROTASK: queueMicrotask
MICROTASK: Promise 2 (nested)
MACROTASK: setTimeout
Priority Comparison Table:
┌────────────────────────────────────────────────────────┐
│ QUEUE PRIORITY ORDER │
│ │
│ 1. 🔴 Synchronous code (call stack) ← FIRST │
│ ▼ │
│ 2. 🟡 Microtask Queue (ALL drained) ← SECOND │
│ • Promise.then() / Promise.catch() │
│ • Promise.finally() │
│ • queueMicrotask() │
│ • MutationObserver │
│ ▼ │
│ 3. 🔵 Render (browser paint, ~16ms) ← THIRD │
│ ▼ │
│ 4. 🟢 Macrotask Queue (ONE per cycle) ← FOURTH │
│ • setTimeout │
│ • setInterval │
│ • I/O callbacks │
│ • requestAnimationFrame │
│ • MessageChannel │
└────────────────────────────────────────────────────────┘
Important: After each macrotask, the event loop drains ALL pending microtasks before picking the next macrotask.
What Puts Things in the Callback Queue?
1. setTimeout / setInterval
// After the timer expires, the callback enters the queue
setTimeout(() => {
console.log("Timer callback in queue");
}, 1000);
// setInterval creates a new entry every interval
let count = 0;
const id = setInterval(() => {
count++;
console.log(`Interval tick ${count}`);
if (count >= 3) clearInterval(id);
}, 500);
Interval tick 1
Interval tick 2
Timer callback in queue
Interval tick 3
2. Event Handlers (DOM Events)
// When user clicks, the event callback enters the queue
document.addEventListener("click", () => {
console.log("Click callback entered the queue when user clicked");
});
// Programmatic dispatch
document.dispatchEvent(new Event("click"));
3. I/O Callbacks (Node.js)
// In Node.js: file read callbacks go through the callback queue
const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
// This callback waits in the queue
console.log("File read callback:", data);
});
4. Network Callbacks (XMLHttpRequest)
const xhr = new XMLHttpRequest();
xhr.onload = function() {
// This callback enters the queue when the network request completes
console.log("XHR callback:", xhr.responseText.substring(0, 50));
};
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.send();
Timer Accuracy — Why setTimeout(fn, 0) Is Never Truly 0ms
const start = Date.now();
setTimeout(() => {
const elapsed = Date.now() - start;
console.log(`Ran after ${elapsed}ms (specified 0ms)`);
}, 0);
// Heavy synchronous work blocks the callback queue processing
let sum = 0;
for (let i = 0; i < 100_000_000; i++) sum += i;
console.log("Sync work done:", sum);
Sync work done: 4999999950000000
Ran after ~150ms (specified 0ms) ← delayed by sync work!
┌──────────────────────────────────────────────────────────┐
│ Why setTimeout(fn, 0) is not instant: │
│ │
│ setTimeout(fn, 0) means: │
│ "Schedule fn after AT LEAST 0ms AND when stack is empty"│
│ │
│ If sync code runs for 150ms, callback is delayed 150ms. │
│ The timer is a MINIMUM delay, not a guarantee. │
└──────────────────────────────────────────────────────────┘
Common Mistakes ⚠️
// ❌ Wrong expectation
console.log("before");
setTimeout(() => console.log("timeout"), 0); // "Runs at 0ms, so next?"
console.log("after");
// Output is NOT: before → timeout → after
// Output IS: before → after → timeout
Mistake 2: Blocking the Callback Queue with Heavy Sync Work
// ❌ Heavy sync work delays ALL queued callbacks
setTimeout(() => console.log("Should run quickly!"), 100);
// This blocks the thread for ~2 seconds
const start = Date.now();
while (Date.now() - start < 2000) {} // Busy wait — BAD!
// The setTimeout callback won't run until AFTER this loop
Mistake 3: Confusing Callback Queue Priority with Microtasks
// ❌ Common confusion: expecting setTimeout(fn, 0) to run before Promise
setTimeout(() => console.log("I thought I'd be first!"), 0);
Promise.resolve().then(() => console.log("I'm actually first!"));
I'm actually first!
I thought I'd be first!
Promises use the microtask queue, which has higher priority than the callback queue.
Interview Questions 🎯
Q1. What is the callback queue in JavaScript?
The callback queue (also called the macrotask queue or task queue) is a FIFO data structure where asynchronous callbacks wait until the call stack is empty. Callbacks from setTimeout, setInterval, DOM event handlers, and I/O operations are placed here when they're ready to run. The event loop moves one callback at a time from this queue to the call stack.
Q2. What is the difference between the callback queue and the microtask queue?
The microtask queue has higher priority and is drained completely after each synchronous task before any macrotask (callback queue item) runs. Microtasks include Promise callbacks (then, catch, finally) and queueMicrotask(). The callback queue processes one item per event loop cycle, after all microtasks are exhausted.
Q3. Why does setTimeout(fn, 0) not run immediately?
setTimeout(fn, 0) schedules the callback in the Web API with a minimum delay of 0ms, but the callback is placed in the callback queue and can only run when: (1) the timer has expired, (2) the call stack is empty, and (3) all microtasks have been drained. If synchronous code is running, the callback waits.
Q4. Is the callback queue FIFO or LIFO?
The callback queue is FIFO (First In, First Out) — callbacks are processed in the order they enter the queue. This is the opposite of the call stack, which is LIFO.
Q5. What puts callbacks into the callback queue?
Web APIs put callbacks into the queue after completing their work: timers (setTimeout, setInterval), network requests (XMLHttpRequest, fetch), DOM event handlers, I/O operations in Node.js, and MessageChannel messages.
Q6. How many macrotasks does the event loop process per cycle?
One. After each macrotask completes, the event loop drains all pending microtasks, allows a browser render if needed, then picks the next macrotask. This ensures UI stays responsive.
Q7. Can microtasks starve macrotasks?
Yes! If microtasks keep generating new microtasks infinitely, macrotasks (and browser rendering) are blocked forever. For example: function addMicrotask() { Promise.resolve().then(addMicrotask); } addMicrotask(); would prevent any setTimeout callbacks or UI updates from ever running.
Key Takeaways 📌
- The callback queue is a FIFO waiting area for async callbacks (macrotasks)
- Callbacks enter the queue from Web APIs after their async work completes
- The event loop moves ONE callback per cycle to the call stack (when stack is empty)
- Microtasks (Promises) have higher priority — they run before any callback queue item
setTimeout(fn, 0) means at minimum 0ms delay, not immediate execution- Heavy synchronous code blocks the callback queue — the queue waits patiently
- After each macrotask, all microtasks are drained before the next macrotask