The Event Loop — Fundamentals
What it is and why it affects you
JavaScript runs your code on one thread with one call stack — one function at a time, to completion. And yet a page handles clicks, timers, and network responses “at once”. The trick is the event loop: a scheduler that runs one task, then picks the next from a queue.
You’ve relied on this without naming it:
console.log('1')setTimeout(() => console.log('3'), 0)console.log('2')// prints 1, 2, 3 — the timeout callback waits its turn even at 0msThe consequence that bites you: while your code runs, nothing else can. No clicks processed, no rendering, no timers. A 3-second loop means a 3-second frozen page — that’s the mechanism behind half the cases in these playbooks.
“Single-threaded” doesn’t mean “no concurrency”, though. I/O (network, disk, timers)
happens off your thread — the platform does the waiting and queues a callback when
ready. JS is single-threaded in your code, concurrent in its I/O. What it can’t do
concurrently is your CPU work — for that you need actual extra threads
(Web Workers / worker_threads).
The mental model
One loop, one rule: finish the current task, drain all microtasks, maybe render, take the next task.
┌─────────────────────────────┐ │ task (macrotask) │ ← script, setTimeout cb, click handler, │ runs to completion, no │ fetch response handler │ interruptions │ └──────────────┬──────────────┘ ▼ ┌─────────────────────────────┐ │ drain microtask queue │ ← promise .then/await continuations, │ (ALL of them, even newly │ queueMicrotask │ queued ones) │ └──────────────┬──────────────┘ ▼ ┌─────────────────────────────┐ │ render if needed (browser)│ ← style, layout, paint — only between └──────────────┬──────────────┘ tasks, never mid-task ▼ next task from queue ──► (loop)Three rules cover almost every situation:
- Tasks are atomic. Nothing preempts running JS. A long task delays input handling and rendering — the browser can only paint between tasks.
- Microtasks run before anything else gets a turn. Every
await/.thencontinuation runs before the next task or render. An infinite chain of microtasks starves rendering just like awhile(true). awaityields, it doesn’t unblock.await fetch(...)frees the loop while the network works (the waiting is elsewhere). Butawait heavyComputation()where the function is plain synchronous JS blocks exactly the same —asyncdoesn’t create threads.
Node’s loop adds phases (timers, poll, check/setImmediate) and
process.nextTick (ahead of even microtasks), but the model is the same: callbacks run
one at a time on one thread; blocking it blocks every request the process is serving,
not just one user’s.
Step through one full turn of the loop yourself:
Reference table
| You write | It’s queued as | When it runs |
|---|---|---|
| synchronous code | current task | now, blocking everything |
promise.then(cb) / code after await | microtask | after current task, before render/next task |
queueMicrotask(cb) | microtask | same as above |
process.nextTick(cb) (Node) | nextTick queue | even before microtasks |
setTimeout(cb, 0) / setInterval | (macro)task | a later loop turn — after render can happen |
setImmediate(cb) (Node) | check phase | after I/O callbacks this turn |
I/O callback (fetch resolution, fs cb) | task (+ microtasks for promise APIs) | when the platform finishes the I/O |
requestAnimationFrame(cb) | rendering step | right before the next paint |
requestIdleCallback(cb) | idle period | when the loop has nothing better to do |
Web Worker / worker_threads code | another thread entirely | in parallel; talks via message passing |
The practical budget: at 60fps the browser paints every ~16ms, and web.dev’s long-task guideline flags any task over 50ms as jank territory. A task of 500ms is a visible freeze.
How this connects to the cases
- A CPU-bound task blocks everything — “parsing a big file / crunching numbers freezes the UI (or starves my Node server)”
- Many async operations at once — “I fire hundreds of promises and things get slow, flaky, or rate-limited”
This area is also the base for the others: frontend jank (The UI freezes, The input lags while typing) and backend stalls (Heavy jobs block requests) are this same loop, blocked in different places.
Primary sources
- MDN — The event loop — the normative description of the run-to-completion model and task queues.
- MDN — Microtask guide — exactly when microtasks run relative to tasks and rendering.
- Node.js docs — The event loop, timers, and process.nextTick — Node’s loop phases and how they differ from the browser.
- Philip Roberts — “What the heck is the event loop anyway?” (JSConf EU) — the canonical 25-minute visual explanation; if the diagram above didn’t click, this will.
- web.dev — Optimize long tasks — the 50ms guideline and yielding patterns.