This question tests whether you understand concurrency without threads. The interviewer wants: the event loop is a task queue plus a dispatcher, and blocking it pauses everything on that thread — not just your function.
The mental model: a single thread runs a loop that repeatedly does two things: take the next task from a queue, and run it to completion. A "task" is a callback: an I/O completion, a timer firing, a setTimeout handler, a promise continuation. The key property: while one task runs, no other task runs. Concurrency comes from never waiting: when a task starts an I/O operation (socket read, file read, database query), the runtime registers interest in the event source and returns control to the loop immediately. The OS or kernel notifies the runtime (epoll/kqueue/IOCP), which enqueues a new task. So the loop is only busy when there is work to do.
The cost model is the important part. Since tasks run to completion, a task that takes 50 ms delays every queued task by 50 ms. A task that blocks on a synchronous read doesn't just delay — it parks the whole loop: no timers, no other I/O completions, no rendering or request handling. At an interview level, name the practical failure: one accidental synchronous file read in a request handler, and every other user's latency spikes by the duration of that read.
Why blocking matters concretely:
- Timers are promises, not guarantees. A
setTimeout(…, 100)fires 100 ms from now if the loop is free; if the loop is blocked for 200 ms, it fires at ~300 ms. - Queue starvation: a tight synchronous loop never yields, so the I/O queue grows without bound — the classic "server is unresponsive but CPU is 100%" incident.
- Backpressure disappears: the runtime keeps accepting connections and enqueuing work; memory grows.
Tradeoffs and edge cases worth naming:
- Microtasks vs macrotasks: promise continuations run in the microtask queue drained after the current task, before the next macrotask — which is why
awaitcan starve I/O if you never yield. - Worker threads/thread pools exist precisely to move blocking work (CPU-heavy or truly synchronous I/O) off the loop thread; the runtime transfers the result back as an event.
- Libuv/Node, browser engines, and .NET's async I/O all use the same substrate: non-blocking syscalls + completion notification + a task dispatcher.
- Synchronous-looking async:
async/awaitis still callbacks; the loop doesn't spin per await, it suspends the function's continuation as a task.
A strong closing: "Blocking the event loop is a latency multiplier for everyone else: one slow task delays every task, because the loop is the only thread that matters."