Skip to content
RungsySign in

The Event Loop

One thread, a queue, and a rule about when it drains — the model that explains every 'why did this run last?'.

45 minDifficulty 3/5runtime · asyncReviewedRead and accepted by a person.

Before this

Why this exists

You add a `console.log` inside a `setTimeout(..., 0)` and it prints *after* code written below it. You wrap something in `Promise.resolve().then()` and it runs before the timeout, despite being written later. None of this is random — there's a precise, learnable schedule, and knowing it is the difference between debugging async code and guessing at it.

The mental model

One chef in a kitchen. He never waits at the oven — he puts something in, sets a timer, and starts the next task. When a timer dings he finishes his current step first, then handles it. One worker, no idle waiting, and nothing is ever interrupted mid-step.

How it works

One thread, and functions run to completion

Your JavaScript runs on a single thread. Once a function starts, it runs to the end — nothing interrupts it partway. That's a real guarantee: no other code can observe your object in a half-updated state, so you never need locks around ordinary code. The price is that a slow function blocks *everything*: no clicks, no rendering, no timers, no incoming requests. In a browser the tab freezes; on a server every concurrent user waits.

The waiting doesn't happen in JavaScript

When you call `setTimeout` or start a network request, JavaScript doesn't wait. It hands the job to the runtime — the browser or Node — which has real threads and OS facilities underneath. Your thread immediately continues to the next line. Later, when the work finishes, the runtime puts your callback in a queue. This is why one thread can handle thousands of concurrent connections: it's never the thing doing the waiting.

The loop itself is dumb and simple

The event loop does one thing forever: if the call stack is empty, take the next callback from a queue and run it. That's the entire algorithm. The subtlety isn't the loop — it's that there is more than one queue, and they don't have equal priority.

Microtasks jump the queue — all of them

Promise callbacks go on the **microtask** queue. Timers, I/O and events go on the **macrotask** queue. After each macrotask, the loop drains the *entire* microtask queue before taking another macrotask — and microtasks added during that drain get processed too, in the same pass. This is why `Promise.resolve().then()` beats `setTimeout(..., 0)`. It's also a trap: a promise chain that keeps scheduling more promises can starve the macrotask queue completely and freeze the page, without ever running an infinite `while` loop.

The mechanism

Note the asymmetry: **all** microtasks, but only **one** macrotask per turn. That's the whole priority story. ```js console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // 1, 4, 3, 2 ``` `1` and `4` are synchronous — they run first, in order. Then the stack empties, microtasks drain, so `3`. Only then does a macrotask run: `2`.

flowchart TD
    A[Call stack empty?] -->|no| A
    A -->|yes| B[Drain ALL microtasks]
    B --> C{More microtasks?}
    C -->|yes| B
    C -->|no| D[Render if browser]
    D --> E[Take ONE macrotask]
    E --> F[Run it to completion]
    F --> A
Diagram source for The Event Loop.

What people get wrong

setTimeout with 0 runs the callback immediately.
It runs after the current synchronous code and after every pending microtask. The delay is a minimum wait before becoming eligible, not a promise about when it runs.
async/await makes code run on another thread.
It's the same single thread. await pauses the function and returns control to the loop. Concurrency here means interleaving, not parallelism. CPU-heavy work in an async function still blocks everything.
Promises are faster than callbacks.
They're scheduled at a higher priority, which is different from being faster. Microtasks are processed before the next macrotask. That's an ordering guarantee, not a speed improvement.
Node is single-threaded, so it can't use multiple cores.
Your JavaScript runs on one thread, but Node has a thread pool for file I/O and crypto, and you can add worker threads or cluster processes. The single thread is about your code's execution model, not about the whole process's use of the machine.

When not to use it

You have genuinely CPU-bound work — image processing, big sorts, crypto.
A Web Worker in the browser, or worker_threads in Node. The event loop helps with waiting, not with computing.
You need hard real-time timing guarantees.
Not JavaScript. Timer callbacks fire when the loop gets to them, which is after whatever is currently running finishes.

Terms

Call stack
The stack of function calls currently executing. The loop only acts when it's empty.
Macrotask
A task from the main queue — timers, I/O, UI events. One runs per loop turn.
Microtask
A promise callback or queueMicrotask job. The whole queue drains between macrotasks.
Starvation
When continuously-scheduled microtasks prevent macrotasks from ever running.
Run-to-completion
The guarantee that a function finishes before any other JavaScript runs.
Blocking
Occupying the thread so the loop can't process anything else.

In an interview

What's the output order of a mix of sync code, setTimeout and promises?

  • all synchronous code first, in written order
  • then the entire microtask queue drains
  • then one macrotask, then microtasks drain again
  • the number in setTimeout is a minimum delay, not a scheduled time

Node is single-threaded — how does it handle thousands of concurrent requests?

  • the thread never waits; I/O is delegated to the runtime and the OS
  • callbacks are queued and run when the stack is empty
  • concurrency comes from interleaving, not parallelism
  • CPU-bound work breaks the model and needs worker threads

Can you recall it?

Why does a promise callback run before a setTimeout of 0, even when the timeout was scheduled first?

Also part of

This idea matters in more than one area — which is usually why it matters.

Sources

Keep track of this

Add Concurrency to your map and Rungsy will schedule reviews so you actually remember it.