# Event Loops

### Event Loops

An event loop is code that waits for events, then runs the right piece of work for each event.

An event can be many things:

| Event | Meaning |
|---|---|
| A socket is ready | Network data can be read or written |
| A timer expired | A scheduled time has arrived |
| A file operation finished | The result is ready |
| A process exited | A child process has ended |
| A signal arrived | The operating system reported something |

The main idea is simple:

```text
while program is running:
    wait for something to happen
    handle what happened
```

That is an event loop.

#### Why Event Loops Exist

A server may have thousands of connections.

A simple threaded design might create one thread per connection. That can work for small systems, but thousands of threads can become expensive. Each thread needs memory, scheduling, and coordination.

An event loop uses a different model. Instead of blocking one thread per connection, it asks the operating system:

```text
Tell me when something is ready.
```

Then the program handles only the connections that can make progress.

This is useful for I/O-heavy programs.

#### Blocking Code

Blocking code waits until an operation finishes.

```zig
const n = try socket.read(buffer[0..]);
```

If no data is ready, the call may wait.

In a simple program, that is fine. In a server with many connections, blocking on one connection can prevent the program from serving others.

#### Evented Code

Evented code waits for readiness.

Instead of saying:

```text
read now, even if it blocks
```

it says:

```text
wake me when this socket can be read
```

Then the event loop waits for many possible events at once.

When one event is ready, the loop handles it.

#### A Very Small Event Loop

Here is a toy event loop. It does not use real operating-system I/O. It only shows the structure.

```zig
const std = @import("std");

const Event = enum {
    timer,
    input,
    shutdown,
};

fn getNextEvent() Event {
    return .shutdown;
}

pub fn main() void {
    var running = true;

    while (running) {
        const event = getNextEvent();

        switch (event) {
            .timer => {
                std.debug.print("timer fired\n", .{});
            },
            .input => {
                std.debug.print("input ready\n", .{});
            },
            .shutdown => {
                std.debug.print("shutdown\n", .{});
                running = false;
            },
        }
    }
}
```

The pattern is:

```zig
while (running) {
    const event = getNextEvent();
    handle(event);
}
```

Real event loops use operating-system APIs instead of `getNextEvent`.

#### The Operating System Does the Waiting

Different operating systems provide different event mechanisms.

| System | Common mechanism |
|---|---|
| Linux | `epoll`, `io_uring` |
| macOS, BSD | `kqueue` |
| Windows | IOCP |
| Portable libraries | wrap these platform APIs |

You do not need to understand all of these at the beginning. The key idea is that the operating system can watch many things at once and report which ones are ready.

Zig’s standard library can build higher-level I/O interfaces on top of these lower-level mechanisms.

#### Event Loops and Async

Async code often needs an event loop.

When async work cannot finish immediately, something must remember it and resume it later.

That “something” is usually an event loop or an I/O backend.

The flow looks like this:

```text
start async read
register interest in socket readiness
return to event loop
event loop waits
socket becomes ready
event loop resumes the read
future completes
await receives result
```

The programmer sees futures and `await`.

The runtime or I/O backend handles the waiting and resuming.

#### Event Loop vs Thread Pool

An event loop and a thread pool solve different problems.

| Tool | Best for |
|---|---|
| Event loop | Many waiting I/O operations |
| Thread pool | CPU work or blocking operations |
| One thread per task | Simple concurrency with fewer tasks |

An event loop is good when most tasks are waiting.

A thread pool is good when tasks need CPU time or must use blocking APIs.

Many real systems use both. An event loop handles sockets and timers. A thread pool handles CPU-heavy work or blocking calls that cannot be made evented.

#### Callbacks

Older event-loop code often uses callbacks.

A callback is a function passed to another function to be called later.

```zig
fn onReadable() void {
    std.debug.print("socket is readable\n", .{});
}
```

The event loop stores `onReadable` and calls it when the socket is ready.

The problem with heavy callback code is that control flow becomes harder to read. The program’s logic is split across many small functions.

Async and futures try to make evented code look closer to ordinary sequential code.

#### Futures

A future is a handle to a result that may arrive later.

In event-loop code, a future often represents work registered with the loop.

```text
future = start operation
event loop waits
operation finishes
future becomes ready
await returns result
```

This lets you write:

```zig
const future = io.async(readFile, .{path});
defer future.cancel(io) catch {};

const contents = try future.await(io);
```

The event loop may be involved behind the scenes, but the code remains readable.

#### Do Not Block the Event Loop

This is the most important event-loop rule.

If code inside the event loop blocks for a long time, the whole loop stops handling other events.

Bad:

```zig
fn handleRequest() void {
    expensiveCpuWork();
    blockingFileRead();
}
```

While this function runs, the event loop cannot process other ready events.

Better:

```text
handle small event
start async operation or send work to thread pool
return to event loop
```

Event-loop handlers should usually be short.

#### Long CPU Work Belongs Elsewhere

Suppose a request needs to compress a large file.

Compression is CPU-heavy. If the event loop performs the compression directly, other clients may wait.

A better design is:

```text
event loop receives request
send compression job to worker thread
event loop continues handling other events
worker completes job
event loop sends response
```

This keeps the event loop responsive.

#### State Machines

Event loops often turn programs into state machines.

A connection may move through states:

```text
waiting for request
reading headers
reading body
processing request
writing response
closed
```

Each event moves the connection forward.

```zig
const ConnectionState = enum {
    waiting_for_request,
    reading_headers,
    reading_body,
    processing,
    writing_response,
    closed,
};
```

The event loop receives readiness events, then updates the connection state.

This is one reason evented systems can feel more complex than simple blocking code. The program must remember where each operation paused.

Async and futures help hide some of this state machine, but the state still exists.

#### Timers

Timers are a common event-loop feature.

A timer says:

```text
wake this task after this amount of time
```

Timers are useful for timeouts, retries, scheduled cleanup, heartbeats, and periodic jobs.

A server might use timers like this:

| Timer use | Example |
|---|---|
| Request timeout | Close connection if no request arrives |
| Retry delay | Try again after 500 ms |
| Heartbeat | Send ping every 30 seconds |
| Cleanup | Remove idle sessions every minute |

Timers are events too. When time passes, the event loop wakes the waiting task.

#### Event Loops Need Explicit Lifetimes

Event-loop code often stores handles, buffers, callbacks, futures, and state objects.

That means lifetime rules matter.

If the event loop may use an object later, that object must stay alive.

Bad shape:

```zig
fn registerRead(loop: *Loop) void {
    var buffer: [1024]u8 = undefined;

    loop.readLater(&buffer);
}
```

The buffer disappears when the function returns.

Good shape:

```zig
const Connection = struct {
    buffer: [1024]u8 = undefined,
};
```

Store the buffer inside an object that lives as long as the connection.

The same rule appeared with threads and async futures: do not pass a pointer to data that may disappear too early.

#### Shutdown

A good event loop needs a shutdown path.

A simple loop has a `running` flag:

```zig
var running = true;

while (running) {
    const event = waitForEvent();

    switch (event) {
        .shutdown => running = false,
        else => handle(event),
    }
}
```

A real system also needs to cancel pending work, close sockets, release memory, join worker threads, and flush logs.

Shutdown should be designed early, not added as an afterthought.

#### Error Handling

Event-loop errors need clear ownership.

Ask:

Who owns this connection?

Who closes it on error?

Who frees its buffers?

Who reports the error?

Who decides whether the whole loop stops?

For example, a single bad client connection should usually close only that connection. But a serious error in the listening socket may stop the server.

Make that distinction explicit in code.

#### Beginner Mental Model

An event loop is a traffic controller.

It does not do all the work itself. It watches many possible events, chooses the next ready one, and dispatches work.

Good event-loop code has these properties:

| Property | Meaning |
|---|---|
| Handlers are short | The loop stays responsive |
| Blocking work is avoided | One task does not freeze all tasks |
| State is explicit | Each connection or task knows where it is |
| Lifetimes are clear | Registered data remains valid |
| Shutdown is planned | Pending work can be cleaned up |

Event loops are the foundation of many high-concurrency programs. They are especially useful when a program handles many I/O operations that spend most of their time waiting.

