Event loop

See how one thread interleaves many I/O tasks with callbacks, and why blocking the loop stalls every request.

In this article

An event loop runs one thread that repeatedly checks for work and invokes callbacks. When a task starts a database query or another I/O operation, it registers what to run when the result is ready and gives control back to the loop.

run task -> start I/O -> register callback -> run another task
                                      ^
                         I/O completes, callback becomes ready

Operating systems expose readiness mechanisms such as epoll on Linux, kqueue on macOS, and IOCP on Windows. The runtime uses them to learn which sockets can make progress, then runs the associated callbacks.

Fig 1Callbacks wait for their turn
STUDY MODEL
Inspect one part at a timePoll
  1. Poll

    Check I/O readiness and the relevant ready-work queues.

  2. Dispatch

    Select and run a ready callback.

  3. Start I/O

    A callback can initiate asynchronous I/O and register a continuation.

  4. Return to loop

    When work is ready, its callback can run on a later turn. Exact phases vary by runtime.

The loop dispatches ready callbacks. Starting I/O does not mean waiting synchronously for it.

How to read this model

A callback indexing table with an I/O holding tray. The object is a teaching analogy. Inspecting a part highlights its role; it does not execute or simulate the software. Cabinet trays are records, workpieces are data, and latches are checks. Unless an explanation states a quantity, compartments and sample workpieces do not denote data counts. Part dimensions are not a quantitative scale.

Why one thread can hold many tasks

The event loop has one execution stack and does not switch between OS threads. Each waiting task stores a small amount of continuation state rather than a full OS thread stack. Thousands of I/O-bound tasks can therefore remain in flight with low scheduling overhead.

The trade-off is strict. A CPU-heavy callback blocks the only loop thread. Every other request waits until that callback returns. Keep callbacks short and move expensive computation to a worker thread or process.

async and await express callbacks

Callback style makes the control handoff visible:

db.query("SELECT * FROM users", userId, function (err, user) {
	if (err) throw err;
	sendResponse(user);
});

async and await flatten the same control flow:

async function handleRequest(req) {
	const user = await db.query("SELECT * FROM users", req.userId);
	sendResponse(user);
}

At await, the function yields. The runtime stores the continuation and resumes it when the promise settles. The function looks synchronous, but the event loop remains available to other tasks.

An async function can be understood as a state machine. Each await is a state transition, and the code after it is the continuation for the I/O operation. If the thread blocks on synchronous CPU work, the state machine cannot advance and every in-flight request stalls.

Compare this model with goroutines and threads.

Keep track of your reading.

Your reading progress stays in this browser.