Skip to article
ALGORITHMICSConcurrency
Concurrency7 min read

Event Loops

One thread, no blocking — and the queue ordering that decides what actually runs next.


A thread waiting on a database call is doing nothing while it waits. Ten thousand concurrent requests, mostly waiting, means ten thousand mostly idle threads and ten gigabytes of stacks.

An event loop says: do not wait. Register what to do when the answer arrives, and go do something else.

The loop

while (running) {
runUntilStackIsEmpty(); // execute current work to completion
drainMicrotasks(); // all of them
runOneMacrotask(); // then exactly one
renderIfNeeded(); // browsers only
}

One thread, one queue of pending callbacks, one rule about which to run next. That rule is the part people get wrong.

What order does this print?

console.log('1');
setTimeout(() => console.log('2'));
Promise.resolve().then(() => console.log('3'));
console.log('4');

The answer is 1 4 3 2. Step through why:

1 / 7
console.log("1")
setTimeout(() => console.log("2"))
Promise.resolve().then(() => console.log("3"))
console.log("4")
call stack main
microtasks
macrotasks
output 1

Synchronous. Runs immediately.

The one rule

Never block the loop. There is one thread; anything slow stops everything.

// A 200ms sort is 200ms of frozen UI, and 200ms of no requests served.
const sorted = hugeArray.sort(compare);

Nothing warns you. The page simply stops responding, and in Node the server stops accepting connections.

The fixes, in order of preference:

And beware the synchronous API: readFileSync, execSync, and crypto.pbkdf2Sync all block the loop completely. In Node these exist for startup scripts, not for request handling.

What you get for free

Because only one callback runs at a time, there are no data races. No mutexes, no atomics, no memory model. Any sequence of statements without an await is effectively atomic.

That is a real simplification, and it is why JavaScript has no synchronized keyword.

Node’s loop has phases

Node’s macrotask side is not one queue but several, visited in order: timers, pending callbacks, poll (I/O), check (setImmediate), close callbacks. Plus process.nextTick, which runs before promise microtasks.

The practical consequences:

Reach for either only when you know which phase you need.