Threads & asyncIntermediate

How an event loop works

“Explain how an event loop lets one thread serve thousands of connections. What can it not do, and how does that failure look?”

What this tests

  • The loop as a readiness-multiplexing wait plus run-to-completion callbacks
  • Why one thread is enough for I/O-bound work: the kernel does the waiting
  • What blocks the loop: CPU-bound work, synchronous syscalls
  • Runtime specifics: Node.js phases, libuv thread pool, microtasks

Answers by level

Read the beginner answer first and notice what is missing.

The loop is a program shape: register interest in a set of descriptors, then call one blocking wait — epoll_wait on Linux, kqueue on macOS/BSD, IOCP on Windows — that returns when *any* of them is ready. For each ready descriptor run its callback to completion, then go back to waiting. One thread suffices because for a network service almost all time is spent waiting, and the kernel can wait on ten thousand sockets as cheaply as on one (I/O Multiplexing: select, poll, epoll, kqueue, IOCP, The Event Loop).

The contract is cooperative: a callback must return quickly, because nothing else runs until it does. Anything that keeps the loop thread busy freezes every connection: a JSON.parse of 50 MB, a synchronous file read, a regex with catastrophic backtracking, a tight loop, a synchronous DNS lookup. The symptom is distinctive — timers fire late, health checks time out, one core is at 100% and the rest are idle, and strace shows almost no syscalls because the thread is not waiting on anything (Async I/O: What `await readFile()` Actually Does).

It also cannot give parallelism. To use more cores you run more loops: Node.js cluster or worker_threads, one process per core behind a shared listening socket. And some operations have no non-blocking form — regular-file reads on Linux always "block" briefly, and getaddrinfo is synchronous — so Node.js (label: Node.js/libuv) runs those on a small thread pool (default size 4) and posts the completion back to the loop.

The trade-off against threads is scheduling: the kernel preempts a misbehaving thread; an event loop trusts every callback. That is why a slow callback hurts *all* clients at once rather than one, and why event-loop latency is the metric to alert on.

Green flags · Red flags

Strong green flag · Describes the event loop as cooperative scheduling and derives the failure mode from that.
Green flags
  • Describes the loop as a multiplexing wait plus run-to-completion callbacks
  • Names epoll/kqueue/IOCP as the mechanism the kernel provides
  • Lists what blocks the loop and the symptom (late timers, one core busy)
  • Knows the thread pool exists for fs/dns in Node.js and that parallelism needs more loops
Red flags
  • "Async means it never blocks"
  • Thinks the event loop is a JavaScript-only concept
  • Cannot say what happens to other connections during a CPU-heavy callback

Follow-up questions

F1
A setTimeout(fn, 100) fires after 3 seconds. What do you look for?
F2
Why does Node.js use a thread pool if it has an event loop?
F3
How do you use 8 cores with an event loop?

Scenario

A Node.js API is marked async everywhere, yet under load one endpoint that builds a 30 MB CSV makes all other endpoints time out. Explain what the async keyword did and did not buy them.

Learn this topic