AlgoMaster Logo

Event Loops, Reactor, and Proactor

15 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A server can ask the kernel which sockets are ready or which asynchronous operations have completed. That information alone does not decide what application code runs next.

The server still needs to:

  • Match each event to a connection or request
  • Invoke the appropriate handler
  • Preserve unfinished protocol state
  • Run timers and internally queued work
  • Return to the kernel before one handler monopolizes the thread

An event loop provides this control structure. It waits for events, dispatches them to handlers, and repeats.

Two classic architectures define what those events mean:

  • In a Reactor, the event says an operation may be performed without waiting.
  • In a Proactor, the event says a previously submitted operation has completed.

Both can support thousands of concurrent connections with a small number of threads. Their main difference is who performs the I/O operation and when.

Event Loops as Execution Policies

An event loop is not a specific system call. It is an application or runtime structure built around an event source such as a readiness multiplexer, completion queue, timer facility, or internal task queue.

A simplified loop looks like this:

The precise ordering varies among runtimes. Some process timers before I/O, others after it, and many use several phases. The stable idea is that the loop repeatedly converts external state changes into short units of application work.

Waiting and running are different phases

While the loop waits in the kernel, its thread consumes no CPU for the idle wait. When the kernel returns events, the same thread executes handlers.

The loop alternates between waiting in the kernel and running handlers. During one handler phase it may run callback A, callback B, and timer C before returning to the wait.

This arrangement is efficient when most connections are idle and each ready handler performs a small amount of work.

It becomes fragile when a handler blocks or computes for too long. The loop cannot return to the wait phase or dispatch unrelated events until that handler yields control.

Concurrency Without One Thread per Connection

In a thread-per-connection design, a blocked call stack naturally remembers the connection's state:

If read_request() waits, that thread's stack remains suspended at the call.

An event loop cannot keep its one thread blocked for every connection. It stores each connection's progress explicitly:

When the connection becomes active, the loop finds this object, advances it as far as possible, saves the new state, and moves to another event.

ConnectionBefore this turnAfter this turn
AREADING_REQUESTWRITING_RESPONSE
BREADING_REQUESTStill reading
CWRITING_RESPONSECONNECTION_CLOSED

The connection objects represent logical concurrency. Only one handler may be executing on the loop thread at an instant, but thousands of connections can be waiting in different states.

The Reactor Pattern

A Reactor is driven by readiness.

The application registers descriptors and interests such as readable or writable. A kernel interface waits across the registered set and returns ready handles. The loop dispatches each ready handle to application code, and that code performs the non-blocking I/O.

  1. Register the socket for readability.
  2. The readiness multiplexer waits.
  3. The socket is reported readable.
  4. The reactor dispatches the read handler.
  5. The handler calls recv().
  6. The handler updates the connection state.

The name reflects the application reacting to a condition.

The architecture contains four practical pieces:

  1. Handles: descriptors for listeners, connections, pipes, or other endpoints
  2. Interest state: which conditions matter for each handle
  3. Demultiplexer: select, poll, epoll, kqueue, or an equivalent wait mechanism
  4. Handlers: application code invoked for the returned conditions

A Reactor read handler

A readiness handler performs the I/O itself:

The handler consumes available input, interprets the result, and returns. If parsing produces a response, it places bytes in the connection's output buffer and enables writable interest.

A writable handler sends from output_sent, advances that offset after partial writes, and disables writable interest once no output remains. Leaving writable interest enabled permanently can keep the Reactor awake because sockets are often writable.

Synchronous Reactor Dispatch

The readiness notification and the subsequent I/O call are separate:

An event says descriptor 17 may be readable, the loop dispatches on_readable(connection_17), the handler calls recv(fd 17, ...), and the result is bytes, EOF, EAGAIN, or an error.

The loop invokes a handler synchronously. The handler runs until it returns. This is often called run-to-completion handler execution, although “completion” here means the callback finishes its current turn, not that the entire client request finishes.

A request can require many turns:

Persistent connection state connects those turns.

The Proactor Pattern

A Proactor is driven by operation completion.

The application submits a concrete asynchronous operation with its buffer and context. The operating system or runtime performs or coordinates the operation. When it finishes, a completion dispatcher invokes the corresponding completion handler.

  1. Submit receive(socket, buffer, request).
  2. The asynchronous provider performs or dispatches the receive.
  3. The completion reports the byte count and the request.
  4. The proactor dispatches the completion handler.
  5. The handler consumes the buffer and submits the next operation.

The application does not first receive a readable event and then call recv(). The receive was already submitted.

The architecture contains:

  1. Operation initiators: code that creates and submits asynchronous work
  2. Asynchronous provider: kernel, runtime, or worker infrastructure that executes the work
  3. Completion source: a queue or notification mechanism carrying results
  4. Completion handlers: code that interprets results and advances application state

A Proactor receive handler

The control flow is completion-oriented:

The handler receives the operation result. It does not call recv() to obtain that completed data. If more input is needed, it submits another receive.

The buffer and request object must remain valid from submission through terminal completion. That explicit in-flight state is the Proactor counterpart to a Reactor's readiness interests.

Reactor and Proactor Compared

The same server can be expressed in either architecture:

QuestionReactorProactor
Event meansOperation may make progressSubmitted operation finished
Operation is issuedInside readiness handlerBefore waiting for completion
Handler receivesReady handle and conditionRequest identity and result
Data buffer chosenWhen handler performs I/OWhen operation is submitted
Partial progressHandler retries or updates interestCompletion reports count; handler may submit remainder
Main kernel abstractionReadiness demultiplexerAsynchronous operation/completion queue

A Reactor asks:

Which connection should I try now?

A Proactor asks:

Which submitted operation finished, and what was its result?

epoll naturally supports Reactor-style networking. Completion interfaces such as I/O completion ports and io_uring naturally support Proactor-style loops.

The mapping is not absolute. A completion-capable interface can also expose poll-like operations, and a runtime can wrap readiness in futures or callbacks. The pattern should be identified from the application control flow, not only from the operating-system API name.

Loading simulation...

Common Hybrid Event-Loop Architectures

One application can use a Reactor for network sockets and completion operations for file reads. A runtime can also expose Proactor-like callbacks while implementing some operations with readiness and others with helper threads.

Four different kinds of work arrive at one place. Anything that blocks the loop delays all four, not just the one that caused it.

The loop normalizes these sources into runnable handlers.

This is why “the service uses an event loop” does not reveal its exact kernel mechanism. Event loop describes the scheduling structure. Reactor and Proactor describe how I/O work enters that structure.

Timers and Internal Tasks

An event loop rarely processes only I/O.

It may also manage:

  • Request deadlines
  • Connection-idle timeouts
  • Periodic maintenance
  • Deferred callbacks
  • Work posted from another thread

Before waiting, the loop finds the nearest timer deadline and uses it to bound the kernel wait:

The next timer is due in 40 ms, so the loop waits for I/O with a timeout no longer than 40 ms. Either the I/O arrives first or the timeout expires, and then the loop dispatches the I/O along with any due timers.

If another thread adds a task while the loop sleeps, it needs a wakeup mechanism. On Unix-like systems, a pipe or eventfd can make the internal task queue visible to the same I/O wait.

The internal queue itself must be synchronized when several threads can post to it, but the loop can remain the single owner of connection state.

Latency Impact of a Slow Handler

Consider three clients whose events are returned together:

B and C each need a millisecond of work and still wait roughly fifty for it, because the loop cannot reach them until A's handler returns.

B and C were ready at the same time as A, but their latency includes A's 50 ms handler.

The same problem affects timers. A timer due after 10 ms cannot run while a handler occupies the event-loop thread for 50 ms.

Common accidental blockers include:

  • A blocking socket or file call
  • Synchronous name resolution
  • Waiting for a lock held by another thread
  • Slow logging to a file or pipe
  • Large serialization or compression work
  • An unbounded loop over one connection's input

Non-blocking sockets do not protect the loop from blocking application code.

An event loop is responsive only when every turn is bounded.

Loading simulation...

Handling Work That Does Not Fit the Loop

Small parsing and response-routing steps can run directly on the loop. Long CPU work and operations without a suitable non-blocking interface need another execution resource.

A common design briefly hands such work to a bounded worker pool and returns to the loop. When the worker finishes, it posts a result back to the loop's task queue.

The loop hands the expensive work away and goes straight back to waiting. The computation still costs the same, but it no longer happens on the thread every other client depends on.

The pool must be bounded so that moving work off the loop does not create an unlimited queue. The event loop should remain the owner of connection state; workers return results rather than mutating a connection concurrently.

Multiple event loops can also run on separate OS threads, with connections partitioned among them. Each loop then executes handlers on one core while the process uses several cores overall.

These techniques add parallel execution capacity. The event loop by itself provides I/O concurrency, not unlimited CPU throughput.

Fairness and Work Budgets

Even non-blocking handlers can monopolize the loop if they process an unlimited amount of ready work.

Suppose one socket continuously receives data. A handler that parses every available message before returning can delay all other clients.

A loop can use budgets:

  • Limit bytes parsed per turn
  • Limit completions dispatched per batch
  • Limit callbacks run from the internal task queue
  • Requeue unfinished application work

Care is required with edge-triggered readiness. If a handler intentionally stops before reaching EAGAIN, it cannot assume the kernel will produce another edge for data that remains ready. The loop must preserve an explicit runnable state or rearm the endpoint according to its notification protocol.

Fairness is cooperative. The operating-system scheduler cannot preempt one callback and run another callback on the same event-loop thread.

A Runnable Reactor Echo Server

Python's standard selectors module exposes a portable readiness multiplexer. On Linux, the default selector normally uses epoll; other systems choose an appropriate native mechanism.

The following server is a small Reactor. Each connection stores pending output, and writable interest is enabled only while that output exists:

Run it:

Connect from another terminal:

The architecture is visible in the code:

  • selector.select() is the readiness demultiplexer.
  • accept_ready() handles listener readiness.
  • service_connection() dispatches readable and writable conditions.
  • Connection.output and peer_closed preserve state across loop turns.
  • recv() and send() perform the actual I/O.
  • Interest in writing is removed after the output buffer drains.

The server uses level-triggered behavior through the selector abstraction, so reading one chunk per turn is valid. The listener drains accepted connections until BlockingIOError, which is the Python form of reaching EAGAIN.

The example omits protocol parsing, connection limits, output caps, deadlines, and graceful shutdown so the Reactor structure remains clear.

Observing Event-Loop Stalls

Two measurements reveal whether handlers keep the loop responsive.

Handler duration measures how long each callback runs from dispatch to return. A slow-handler log should include the callback type and connection or request identity.

Loop lag compares when a timer was scheduled to run with when the loop actually began running it:

Loop lag includes time spent in earlier handlers, task queues, and scheduler delay. It is more useful than CPU utilization alone: one blocked loop can have low CPU usage while all of its connections wait.

Measurement code must also be lightweight. Synchronously logging every normal handler can become the stall it is trying to diagnose.

Summary

An event loop waits for I/O, completions, timers, and internal tasks, then dispatches short handlers. It represents many waiting operations through explicit request and connection state instead of one blocked thread stack per connection.

A Reactor receives readiness events and invokes handlers that perform non-blocking I/O. A Proactor receives results for operations submitted earlier and invokes completion handlers that consume those results and submit follow-up work.

One event-loop thread executes handlers sequentially. Any blocking call or long computation delays unrelated connections and timers, so each turn must remain bounded. CPU-heavy or blocking-only work can be handed to bounded workers, while the loop retains ownership of connection state.

Real applications can combine readiness, completion, timers, and worker results in one loop. The pattern is determined by application control flow, not merely by the name of the underlying kernel API.

The central mental model is:

A Reactor says “the operation may proceed”; a Proactor says “the operation finished”; the event loop decides which handler runs next.

Quiz

Event Loops, Reactor, and Proactor Quiz

5 quizzes