Skip to main content

Command Palette

Search for a command to run...

Part 1: The Question Tutorials Skip

Updated
18 min read

What no one teaches you about the JavaScript Event Loop — Part 1 of 8


A puzzle to start

Open any page in your browser. Open DevTools, go to the console, and paste this:

document.body.addEventListener('click', () => {
    alert('clicked');
});
while (true) {}

The page freezes. You click anywhere on the page. Nothing happens. No alert. No visual response. The page is dead. You eventually have to kill the tab.

The usual explanation: "JavaScript is single-threaded, so the click can't run while the loop is running."

That sentence is repeated in every event loop tutorial. It's not exactly wrong. But it leaves a hole. Because the click did happen. Physically. You pressed the mouse button. The computer received the signal. The browser received the event. The click didn't disappear — it can't have, because computers don't lose signals. So where is it? Where does a click live when nothing is listening?

Try a second experiment to make this sharper:

let clicked = false;

document.body.addEventListener('click', () => {
    clicked = true;
});

console.log('starting 5-second freeze; click anywhere during it');

const start = Date.now();
while (Date.now() - start < 5000) {
    // burn 5 seconds
}

console.log('clicked during the freeze?', clicked);

setTimeout(() => {
    console.log('clicked, checked one tick later?', clicked);
}, 0);

Click anywhere on the page during the freeze. Watch the console.

The first console.log says false. The click "didn't happen" yet — the listener never ran. But moments later, the setTimeout callback fires, and now clicked is true. The click was somewhere all along. It just wasn't reachable during the loop.

Three questions fall out of this:

  • Where was the click? It can't have been nowhere. Some piece of memory held it. Where?

  • Why was it unreachable? Why couldn't the listener run even though the click data existed somewhere?

  • What changed in those few milliseconds between the two console.logs? Why was the click suddenly accessible?

Every JavaScript developer knows "the event loop" handles this. Most can't say what "the event loop" is. The phrase has become a sentence-shaped placeholder that closes the conversation without explaining anything.

This series opens it back up.


What this series is

Eight posts. About 2.5 hours of reading total. Starting from "what is a CPU, physically" and ending with "here is the exact spec algorithm that schedules every JavaScript callback in a browser, and here is how it differs in Node.js."

The promise: by the end, the three questions above are not vocabulary problems. They are mechanical questions with mechanical answers, and you can describe the answers using named components doing named operations. No hand-waving. No "and then the browser does something."

What you bring: any working knowledge of JavaScript. You've used fetch, setTimeout, Promises, async/await. You can predict the output of simple async code most of the time. That's enough. No C, no Python, no operating systems background, no compilers.

Why we start at the silicon

You might be wondering why a series about JavaScript starts with RAM and CPUs.

Here's the honest reason. Every JavaScript developer I've watched try to understand the event loop hits the same wall at the same place. They learn "the call stack" and "the callback queue" and "the microtask queue." They learn that microtasks drain before tasks. They memorize the order. Then a real question shows up — where does a click live when nothing is reading it? why does await not block the page? why does fetch's response not go directly into the microtask queue? — and the memorized rules fail. Because the rules were never the explanation. The rules were the consequences of an explanation that lives one layer down.

Consider what you're actually claiming when you say "JavaScript is single-threaded." You're using the word thread. A thread isn't a JavaScript thing. It's an operating system thing — specifically, a unit the OS schedules onto CPU cores. If you don't know what a thread is at the OS level, the phrase "JavaScript is single-threaded" is just a sound. It doesn't carry information. You can't reason with it.

Same for "the main thread is blocked." Blocked from what? Blocked by what? The answer is "the thread is in a state called sleeping or running something else, and won't return to your JavaScript until the OS schedules it again." You can't follow that sentence without knowing what threads, states, and scheduling are.

Same for await. When you write await fetch(url) and your function "pauses," nothing in JavaScript paused it. The thread didn't pause — the thread is reused for other work immediately. What paused is a piece of bookkeeping that V8 saved to the heap, with instructions for the event loop to call back into your function later. To understand that, you need to know what V8 is doing on the thread, what the heap is, and what the event loop actually is at the level of "this is the syscall it makes when it has nothing to do."

The shortest honest answer to "why does the event loop work the way it does" is: because the operating system gives the thread one specific way to wait for many things at once, and the event loop is built on top of that mechanism. The mechanism has a name. It has steps. It has a queue of file descriptors and a syscall that returns when any of them is ready. None of this is exotic — it's the same mechanism every server uses to handle thousands of connections. But none of it is JavaScript, and you can't see it from inside the JavaScript layer.

So this series spends three parts on the substrate (RAM, CPU, threads, the OS-level wait mechanism), then four parts building the event loop on top of that substrate, then one part on how Node.js diverges. The substrate parts are not background. They are the actual explanation. The event loop parts are just the consequences playing out.

If you're impatient — if you want to start at Part 5 where the event loop algorithm appears — you can. Each part stands alone enough to be read independently, with links back to prior parts for prerequisites. But the readers who get the most out of this are the ones who go in order. Because the questions Part 5 answers are not interesting on their own. They're interesting because of what they expose about the operating system underneath, and that exposure is what makes the model stick.

What this series does not cover

  • How V8 turns JavaScript text into machine code (separate series).

  • How the browser turns a URL into a rendered page (separate series).

  • React/Vue/etc. scheduling internals (touched briefly in Part 7, not deep-dived).

  • WebAssembly, Web Workers in depth (mentioned where relevant).

What this part covers

Just enough about RAM, the CPU, and what a "running program" physically is, so the rest of the series has a foundation. Part 1 doesn't even reach the word thread. That's Part 2. But by the end of Part 1, "thread" will have somewhere to land — because you'll know what the CPU does, what RAM holds, and what "running" actually means at the bottom of the stack.


The myth we're going to dismantle (gently)

You've heard some version of this:

"The CPU runs your code."

It's the kind of sentence that feels too obvious to question. Of course the CPU runs the code. What else would?

Here's the problem. If the CPU "runs the code," then the CPU is "doing" something — making decisions, following instructions, interpreting your intent. The CPU starts to feel like a tiny intelligent agent sitting inside your computer, looking at JavaScript and figuring out what to do.

That mental model is wrong. Not subtly wrong. Wrong at the foundation. And every event-loop confusion downstream stems from imagining the CPU as smarter than it is.

The CPU is, mechanically, one of the dumbest things in your computer. It does exactly one thing in a loop, forever, with no awareness of anything. We're going to spend this post establishing what that one thing is — because once you see how dumb the bottom of the stack is, every layer above it stops feeling magical.

Let's start with RAM.


RAM, mechanically

You probably know RAM as "memory in the computer." Let me make that concrete.

A modern laptop has 8, 16, 32 gigabytes of RAM. A gigabyte is roughly a billion bytes. A byte is 8 bits. A bit is a single 1 or 0.

So when we say 16 GB of RAM, we mean: there are roughly 17 billion little circuits in your computer's memory chips, each capable of storing one byte (8 bits) of data. The bits themselves are stored as electrical charges — a tiny capacitor either holds charge (1) or doesn't (0), grouped eight at a time.

Here's the part that matters most:

Every byte has a numbered address. The first byte sits at address 0. The next at address 1. Then 2. Then 3. All the way up to roughly 17 billion.

That's the entire structure of RAM. A gigantic numbered array of bytes.

Address       Byte (8 bits)
─────────────────────────────
0             01101000
1             01100101
2             01101100
3             01101100
4             01101111
5             00000000
...
17,179,869,183  (the last byte)

The CPU talks to RAM by sending addresses across a bundle of wires (the address bus) and receiving bytes back across another bundle (the data bus). "Give me the byte at address 0x4F3A" — RAM returns one byte. "Store this byte at address 0x4F3A" — RAM saves it.

That's all RAM is. A huge array. Addresses on one side, bytes on the other. It doesn't think. It doesn't interpret. It just stores.

A side note on names, because they trip people up. The unit is called a byte because in 1956 an IBM engineer named Werner Buchholz coined the term as a deliberate respelling of "bite" — a small group of bits the CPU consumed in one go. The size wasn't fixed at first. Different machines used 6-bit, 7-bit, 9-bit "bytes." IBM's System/360 in 1964 standardized 8 bits, and the industry copied IBM because IBM had the market. ASCII (the original character encoding) fit in 7 bits, so 8 bits gave one character plus a parity bit. The name byte is from "bite," with a y to avoid being confused with bit. None of this is physics. It's industrial accident calcified into universal convention. Today, "byte means 8 bits" is one of the few near-universal agreements in computing.

If you remember nothing else about RAM, remember this: bytes have addresses, and the CPU asks for them by number. That's the contract between RAM and CPU. Everything else is built on top.


The CPU, mechanically

You probably know the CPU as "the chip that does the work." Let me make that concrete too.

A CPU is a circuit. Not "like" a circuit — literally a circuit, with billions of transistors etched onto silicon at nanometer scale. Transistors are tiny electrical switches. Arrange enough switches in the right pattern and you can build logic gates (AND, OR, NOT). Arrange enough gates and you can build adders, comparators, decoders. Arrange enough of those and you can build a CPU.

For years I treated "the CPU runs the code" as if running were a primitive operation. I never asked what running meant. That single unexplored word was where every event-loop confusion I had eventually rooted.

What the CPU actually does is this. Every clock cycle — roughly a billion times a second on a 1 GHz CPU, three billion times a second on a 3 GHz CPU — it performs exactly one job:

   ┌─────────────────────────┐
   │   1. FETCH the byte(s)                 │
   │   at the address in                    │
   │   RIP (a register)                     │
   └──────────┬──────────────┘
   ┌──────────▼──────────────┐
   │   2. DECODE                            │
   │   (figure out what                     │
   │   instruction it is)                   │
   └──────────┬──────────────┘
              │
   ┌──────────▼──────────────┐
   │   3. EXECUTE                           │
   │   (do the operation:                   │
   │   add, store, jump,                    │
   │   etc.)                                │
   └──────────┬──────────────┘
              │
   ┌──────────▼──────────────┐
   │   4. ADVANCE RIP                       │
   │   (or set it elsewhere                 │
   │   if the instruction                   │
   │   was a jump)                          │
   └──────────┬──────────────┘
              │
              └─────── back to step 1

This is called the fetch-decode-execute cycle, and it is the entire job of the CPU. It does this billions of times per second. Forever. Until you turn the computer off.

A few terms inside that diagram:

  • RIP (on x86-64 CPUs) — Register Instruction Pointer. It's a small piece of memory inside the CPU itself, holding the address of the next instruction to fetch. ARM CPUs call this same thing PC ("program counter"). Different names, same role: "where am I in the code right now?"

  • Register — A storage slot built physically into the CPU silicon. Not RAM. Tiny — typically 8 bytes — but extremely fast to access (under 1 nanosecond, versus 50-100 nanoseconds for RAM). Modern x86-64 CPUs have 16 general-purpose registers (named RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8 through R15), plus RIP for the instruction pointer and a handful of others for specialized roles. ARM64 has 31 general-purpose registers. The CPU uses registers as scratch paper for whatever it's currently computing.

  • Instruction — A pattern of bytes the CPU's decoder recognizes as meaning a specific operation. On x86, an instruction can be anywhere from 1 to 15 bytes long. On ARM, instructions are a fixed 4 bytes each (with a separate 2-byte "compact" mode). The byte pattern 0x48 0x89 0xD8 on x86 decodes to "move the value in register RBX into register RAX." The byte pattern 0xC3 decodes to "return from this function." Each pattern triggers a specific set of wires inside the CPU.

So when you read "the CPU executes an instruction," what's literally happening is:

  1. The CPU reads a few bytes from RAM at the address RIP holds.

  2. A piece of circuitry called the decoder looks at those bytes and activates a specific set of internal control wires.

  3. Those wires cause specific operations: arithmetic in the ALU (arithmetic logic unit), a memory read, a memory write, a register-to-register move, a jump (which changes RIP to a different value).

  4. RIP advances by the instruction's length, or gets set to wherever the jump told it to go.

  5. Cycle repeats.

There is no interpretation here. There is no judgment. The decoder is hard-wired. It physically responds to bit patterns in the only way it knows how. The CPU has no concept of "a program" or "a function" or "JavaScript." It doesn't know about variables or types or scopes. It only knows: fetch the byte at RIP, decode, execute, advance, repeat.

A 3 GHz CPU does this three billion times per second. Per core. A modern laptop has 4-16 cores. So your laptop is, right now, performing on the order of 50 billion fetch-decode-execute cycles every second. None of those cycles knows what it's part of. They just happen, one after another, forever, until you shut down.

This is what people mean when they say "the CPU is dumb." It really is. The decoder is fixed. The cycle is fixed. The CPU does the same thing in 1995 as in 2025 — just billions of times faster. All the apparent intelligence in your computer comes from layers above this. None of it comes from the CPU itself.


What "a running program" actually is

Now we can answer a question you probably never asked because it felt too obvious: what does it mean for a program to be running?

A program file on your disk — chrome.exe, node, /bin/bash — is just bytes. Bytes describing machine code, plus some metadata describing where things should be loaded, what other libraries are needed, where execution starts.

When you "launch" the program, the operating system does the following:

1. Reads the program's bytes from disk.
2. Copies those bytes into RAM at some address (say 0x400000).
3. Sets a CPU's RIP register to point at the first instruction
   (say 0x400420 — somewhere inside the loaded bytes).
4. Lets the CPU continue its fetch-decode-execute cycle.

That's it. That's launching a program.

   ┌────────────────────────────┐
   │  Disk: chrome.exe                          │
   │  (1.2 GB of bytes)                         │
   └─────────────┬──────────────┘
                 │  read & copy
                 ▼
   ┌────────────────────────────┐
   │  RAM: bytes at 0x400000...                 │
   │                                            │
   │  These bytes are now the                   │
   │  program's machine code.                   │
   └─────────────┬──────────────┘
                 │  set RIP to 0x400420
                 ▼
   ┌────────────────────────────┐
   │  CPU: fetches byte at                      │
   │  RIP, decodes, executes,                   │
   │  advances RIP, repeats...                  │
   │                                            │
   │  Chrome is now "running."                  │
   └────────────────────────────┘

A "running program" is, mechanically:

  • Some bytes sitting in RAM (the program's machine code, plus data the program uses).

  • A CPU whose RIP points somewhere inside those bytes.

  • The CPU executing its eternal cycle, fetching from RAM, decoding, executing.

There is no separate "running" thing happening anywhere. There is no daemon supervising the program. There is no metadata saying "this program is alive." There is only: bytes are loaded, RIP points at them, the CPU's cycle is consuming them.

When you "close" the program, what physically happens is the OS sets RIP to point elsewhere (into operating-system code that handles cleanup) and marks the bytes in RAM as free to be overwritten. The "running" stopped because nothing is fetching from those bytes anymore. The bytes are still in RAM for a while — they just don't matter, because nothing's reading them.

This is the model. Running is not a thing programs do. Running is what the CPU does to programs — and it does it to one program at a time, per core, with no awareness of what the program is or what it represents.


The cliffhanger

Here's where Part 1 ends, with one question that motivates everything that follows.

A CPU core can only fetch one instruction at a time. A 4-core CPU can fetch four instructions at once — one per core — but no more. So if your computer has a 4-core CPU and you're running Chrome, Slack, your code editor, a music player, a system monitor, plus the operating system itself — that's six or seven programs (and more) wanting to use four cores. The math doesn't work. There aren't enough cores.

Yet they all seem to run at the same time. Your music plays while you scroll. Your editor responds while a download progresses. The system clock keeps ticking while you click.

What's actually happening?

The first piece of the answer is something called a thread. A thread is the unit the operating system schedules onto CPU cores. The OS rapidly cycles between threads — running one for a few milliseconds, pausing it, running another, pausing it, switching back. Done fast enough, it looks continuous. Done with the right structure, it produces the entire illusion of "many things happening at once."

When I finally understood that the CPU is dumb — that the entire system is dumb circuits all the way down — every layer above became reasonable. I'd been looking for intelligence in the wrong place.

The thread is where intelligence about scheduling lives. Threads are why JavaScript can be "single-threaded" while the rest of your computer is multi-tasking. Threads are why a while (true) {} loop freezes one tab but not another. Threads are why the click in our opening puzzle had somewhere to go even when nothing was reading it.

We start there in Part 2.


What you should hold

Two things, only:

1. RAM is a numbered array of bytes. The CPU asks for bytes by address. RAM hands them back. There is no semantics in this exchange. Just addresses and bytes.

2. The CPU is a circuit that fetches, decodes, executes, and advances — billions of times per second, with no awareness of anything. A "running program" is bytes in RAM with RIP pointing at them. That's the whole thing.

If these feel obvious to you, good. They're supposed to. The point is not to learn something new about RAM. The point is to make sure RAM and the CPU are concrete in your head — not vocabulary, not metaphors, but physically locatable things doing physically describable jobs. Every layer of confusion above this dissolves when you can return to "bytes in an array" and "RIP in a register" as the floor of the explanation.

In Part 2, we add the next layer: how the OS hides the fact that you have only 4 cores from a system pretending to run 200 programs at once. That layer is where your tab freezing finally starts to make sense — and where the click in our opening puzzle starts to have an address we can point to.


Next: Part 2 — What "Single-Threaded" Actually Means.

This series builds from silicon up to the JavaScript event loop. If you've watched Lydia Hallie or Philip Roberts' talks and felt like the explanation stopped short, this is the long-form version that keeps going. Eight parts, ~2.5 hours, all the way down to the spec algorithm and back up.

What no one teaches you about the JavaScript Event Loop

Part 1 of 4

A first-principles walk through the JavaScript event loop. Threads, kernel scheduling, epoll, V8 as a library, the HTML spec algorithm, Promise internals, and why await is a state machine — covered mechanically, with the assumption that the reader wants to understand, not memorize.

Up next

Part 2: What "Single-Threaded" Actually Means

What no one teaches you about the JavaScript Event Loop — Part 2 of 8 A new puzzle In Part 1, an infinite loop froze the page. The click vanished. We left with a question: what does "JavaScript is si