Library / SDK
mitchellh/libxev avatar
mitchellh/libxev

libxev: a cross-platform event loop in Zig with a C API

libxev is a cross-platform, high-performance event loop that provides abstractions for non-blocking IO, timers, events, and more and works on Linux (io_uring or epoll), macOS (kqueue), and Wasm + WASI. Available as both a Zig and C API.

3,586 stars192 forksZigMIT

At a glance

What is it?
libxev is a proactor-style event loop for Linux, macOS and Wasm that ships both a Zig and a C API. It is stable enough for projects like Ghostty, but several features on its own roadmap are still missing.
Who is it for?
Adopt libxev if you are writing Zig or C and want a proactor-style loop that runs on Linux, macOS and Wasm without pulling in a dependency tree. Do not adopt it if your target is Windows, since the README lists the Windows backend as planned and not shipped, or if you need pipe, signal or filesystem-event watchers, which appear under the roadmap rather than the features list.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 76 days ago.
What is it written in?
Mainly Zig, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem libxev solves, and who it is for

Zig had no generalized event loop comparable to libuv in feature coverage, and that gap is the stated reason the library exists. The author also wanted a loop built around io_uring's design patterns rather than around readiness notification, and one that could compile to WebAssembly without dragging in a toolchain as heavy as Emscripten. The README calls the motivation "scratching my own itch", which is worth taking at face value: this is a library shaped by the needs of the projects that use it, not by a committee.

The audience follows from that. If you are writing a Zig program that needs timers, sockets, files or processes without blocking, libxev is the natural candidate. If you are writing in another language but can call a C API, the exported C surface makes the same loop reachable, and the repository ships examples/_basic.c alongside examples/_basic.zig, so both entry points are exercised in the tree. If you need a loop that runs inside a browser or under WASI, the WebAssembly target is the reason to look here rather than at libuv. The README names Ghostty and zml as daily users and describes the project as "Stable for most use cases", while noting that less well-used corners of the library exist. That is an honest split: the core paths are exercised at scale, the edges less so.

Proactor semantics and the backend per platform

libxev is a proactor, not a reactor. You submit work to the loop and the loop tells you when the work is complete, rather than telling you a descriptor is ready to read or write. That distinction shapes every API in the library, and the README explicitly credits a TigerBeetle blog post about abstracting over io_uring and kqueue for the design.

The backends differ by platform. On Linux the library can use io_uring or epoll. On macOS it uses kqueue. On WebAssembly and WASI it uses poll_oneoff, with both threaded and non-threaded runtimes supported. Because kqueue has no reliable non-blocking interface for local file operations, those operations are pushed to a thread pool instead. That is the mechanism to understand before you benchmark anything: a file read on macOS is not the same shape of work as a socket read, and the loop will route it differently. The thread pool is optional and configurable, can be shared across multiple threads and loops, and is also exposed for your own background tasks.

There is no runtime allocation anywhere in the library, which the README ties to predictable runtime performance and suitability for embedded environments. Zig's tree shaking means unused watchers are not compiled into the binary at all. If you never touch UDP, the UDP code is not in your executable. The library has no runtime dependencies beyond the OS APIs, and the C library depends on libc.

Building libxev and running a first timer

The repository is a Zig package with build.zig and build.zig.zon at the top level, plus a flake.nix and shell.nix for Nix users. There are no published release artifacts listed, so the practical route is to depend on the repository directly. In a Zig project, add it to build.zig.zon as a dependency and import the module as xev. The README's own example is the smallest useful program: a single five second timer.

zig
const xev = @import("xev");

pub fn main() !void {
    var loop = try xev.Loop.init(.{});
    defer loop.deinit();

    const w = try xev.Timer.init();
    defer w.deinit();

    var c: xev.Completion = undefined;
    w.run(&loop, &c, 5000, void, null, &timerCallback);

    try loop.run(.until_done);
}

The callback returns a xev.CallbackAction, and returning .disarm tells the loop not to resubmit the completion. Run the loop with .until_done and the process exits once no armed completions remain. The README notes that this example is deliberately trivial and meant to convey the feel of the API rather than a practical pattern.

The C surface is the same program through a different door. The README shows xev_loop_init returning non-zero on failure, a xev_watcher for the timer, and a callback returning XEV_DISARM.

c
#include <stddef.h>
#include <stdio.h>
#include <xev.h>

xev_cb_action timerCallback(xev_loop* loop, xev_completion* c, int result, void *userdata) {
    return XEV_DISARM;
}

int main(void) {
    xev_loop loop;
    if (xev_loop_init(&loop) != 0) {
        printf("xev_loop_init failure\n");
        return 1;
    }
}

For a fuller picture, the examples directory contains async.c, million-timers.c and threadpool.c, which cover the thread pool and higher concurrency than the basic example. Those files are the closest thing to a tutorial in the tree; the README itself does not walk through a build command, so expect to read build.zig to see how the examples are wired.

Where libxev is the wrong tool

The roadmap is the limitation list, and it is long enough to matter. Pipe watchers, signal handlers and filesystem events are all listed as missing features rather than shipped ones. If your design depends on inotify-style file watching or on catching SIGTERM through the loop, libxev does not offer it today and you would be building that layer yourself. The Windows backend is also listed as planned and coming soon, which means the cross-platform claim currently covers Linux, macOS and Wasm, not the three desktop platforms a reader might assume from the word. The README's feature list mentions Windows in the opening paragraph while the platform list in the same section omits it, so read the platform line, not the summary line.

A second boundary is the Zig std.Io story. libxev does not implement the std.Io interface and does not accept a std.Io for its operations. It calls the OS directly, and in the few places it needs a std.Io, such as mutex operations, it uses the global std.Io.Threaded implementation. The README is explicit that supporting std.Io properly would require breaking the API significantly, and that the interface is still new and unstable and does not expose everything needed for parity. If you are building on Zig 0.16 or later and want your IO to flow through std.Io for testability or for the standard library's abstractions, libxev will sit outside that model.

Performance is the third place to be careful. The README states plainly that not much optimization work has been done and that specific benchmark results are withheld until there is a better environment to run them in. The claim offered is a broad generalization: you should not notice a slowdown against other major event loops. That is a reasonable statement and an unverified one, and it differs by feature.

libxev against libuv, and the io_uring versus kqueue split

libuv is the obvious comparison and the README makes it directly, saying Zig lacked a generalized event loop comparable to libuv in features. The difference in approach is the proactor model. libuv is built around readiness: you register interest in a descriptor, the loop wakes you when it is ready, and you perform the IO. libxev submits the operation and notifies you on completion, which is how io_uring works natively and how the library emulates the same shape on kqueue and on poll_oneoff. In practice that means your callbacks receive a result rather than a readiness signal, and the error type is attached to the completion.

The second difference is the target set. libuv carries a Windows backend; libxev does not yet. libxev compiles to WebAssembly and WASI; libuv is not aimed at that. So the choice is not really about which is faster, since the README declines to publish numbers, but about which platforms and which programming model you need. If Windows is on your list, libuv is the safer answer today. If Wasm is on your list, libxev is one of the few options that treats it as a first-class target. The io_uring versus kqueue question people search for is really the same question at the syscall level: io_uring accepts submissions, kqueue reports readiness, and libxev's job is to make those two look alike from above.

Maintenance, licensing and the cost of upgrading

The repository is not archived and the last push was on 2026-07-17. The README presents a roadmap of features still to come, so the project is moving, but there are no retrieved releases, which means you are tracking a branch rather than pinned versions. That is the real upgrade cost here: without tagged releases, updating means pulling from main and reading the diff, and the README already warns that a std.Io integration would require breaking the API significantly. Budget for that break rather than assuming the current call signatures survive the next Zig release.

The library is MIT licensed, and the README states it has no dependencies other than the built-in OS APIs at runtime, with the C library depending on libc. That combination keeps the licence surface simple: there is no bundled third-party code to audit. It also means cross-compilation is straightforward, which the README lists as a direct benefit of being dependency-free. None of this is legal advice; if your organisation has specific requirements around licence compatibility, check the LICENSE file in the repository root and get your own review.

Editorial conclusion

Adopt libxev if you are writing Zig or C and want a proactor-style loop that runs on Linux, macOS and Wasm without pulling in a dependency tree. Do not adopt it if your target is Windows, since the README lists the Windows backend as planned and not shipped, or if you need pipe, signal or filesystem-event watchers, which appear under the roadmap rather than the features list. Before committing, verify that the backend you intend to ship on is the one your build selects, and check how your code behaves when a file operation is pushed to the thread pool, because that path changes the timing characteristics of the call.

Frequently asked questions

Which platforms does libxev support?

The README lists Linux with io_uring or epoll, macOS with kqueue, and WebAssembly plus WASI using poll_oneoff with threaded and non-threaded runtimes. The Windows backend is listed under the roadmap as planned and coming soon, so it is not part of the supported set yet.

How do I install libxev?

There are no retrieved releases, so the practical route is to depend on the repository directly. The top level contains build.zig and build.zig.zon for Zig projects, and include/ holds the C header used by examples/_basic.c.

What is the purpose of an event loop?

libxev's README describes it as a unified abstraction for non-blocking IO, timers, signals and events that works across macOS, Windows, Linux and WebAssembly. In libxev's case the loop is a proactor: work is submitted to it and the caller is notified of completion rather than of readiness.

Is libxev usable from C, or only from Zig?

It exports a C-compatible API in addition to the Zig one, which the README notes makes it usable from any language that can talk to C. The repository includes examples/_basic.c next to examples/_basic.zig.

Does libxev work with Zig's std.Io interface?

No. The README states that libxev does not implement std.Io and does not take a std.Io for its operations, calling the OS directly instead, and that supporting it properly would require breaking the API significantly.

Official sources

  1. Issues
  2. License: MIT
  3. mitchellh/libxev on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mitchellh-libxev.svg)](https://hysenlabs.com/projects/mitchellh-libxev)