# Eio: direct-style concurrency for OCaml 5 without a monad in sight

> An effects-based IO stack for OCaml 5 that lets you write concurrent code in ordinary style, with a generic cross-platform API sitting on optimised backends including a Linux io_uring one. It is the OCaml community's answer to the question of what Lwt and Async would look like if OCaml had effects.

**ocaml-multicore/eio** — Effects-based direct-style IO for multicore OCaml

- Repository: https://github.com/ocaml-multicore/eio
- Stars: 735 · Forks: 90
- Language: OCaml
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/ocaml-multicore-eio

## Why effects changed what concurrency code can look like

The motivation section explains the whole design in a few paragraphs, and it starts from a problem most OCaml programmers will recognise.

The Unix library provided with OCaml uses blocking IO operations, and is not well suited to concurrent programs such as network services or interactive applications. For many years, the solution was to use libraries such as Lwt and Async, which provide a monadic interface. These libraries allow writing code as if there were multiple threads of execution, each with their own stack, but the stacks are simulated using the heap.

OCaml 5 added support for effects, removing the need for monadic code here. Eio implements similar functionality to Lwt or Async, but using effects rather than monadic concurrency.

The advantages the project lists are worth quoting in substance. It's faster, because no heap allocations are needed to simulate a stack. Concurrent code can be written in the same style as plain non-concurrent code. Because a real stack is used, backtraces from exceptions work as expected. And other features of the language, such as try ... with ..., can be used in concurrent code.

That fourth point is the one that changes day to day work. Under a monadic interface, exception handling has to be threaded through the bind chain, and a try block in the middle of a callback has to return the right monadic value. With effects, the handler is somewhere else in the call stack, so the try block is just a try block.

Eio also says it aims to be easy to use, secure, well documented, and fast, and that a generic cross-platform API is implemented by optimised backends for different platforms.

## The package split, and which backend you actually run

The package layout is the architecture, and it is a sensible one. Six packages cover the whole system.

Eio provides concurrency primitives, promises and the rest, plus a high-level, cross-platform OS API. Eio_posix provides a cross-platform backend for these APIs for POSIX-type systems. Eio_linux provides a Linux io_uring backend for these APIs. Eio_main selects an appropriate backend, such as eio_linux or eio_posix, depending on your platform. Eio_js allows Eio code to run in the browser, using js_of_ocaml. Eio_windows is for use on Windows, and the readme marks it as incomplete with a help wanted link.

So the layering is: your code talks to Eio's generic API, a backend implements it for a platform family, and Eio_main picks the backend at runtime.

The io_uring backend is the most interesting of the three real ones, and the reason is stated in the motivation section. Modern operating systems provide high-performance alternatives to the old Unix select call. For example, Linux's io_uring system has applications write the operations they want to perform to a ring buffer, which Linux handles asynchronously, and Eio can take advantage of this.

That is a different mechanism from the classic readiness-notification model, and it means the Linux backend is not just a faster version of the POSIX one. It is a different system call interface underneath a shared API. The io_uring path also comes with real operational consequences, because ring buffer configuration and kernel version requirements are not things the generic API can hide.

Eio_windows being incomplete is the significant gap in the matrix, and it is stated plainly rather than buried.

## Installing OCaml 5 and Eio, and running the first program

You will need OCaml 5.2.0 or later. You can either install it yourself or build the included Dockerfile.

For the manual route, the readme asks for opam 2.1 or later, checked with opam --version, and then a switch:

```bash
opam switch create 5.5.0
```

Then Eio itself, and utop if you want to try it interactively:

```bash
opam install eio_main utop
```

For the latest unreleased development version, the readme points at HACKING.md in the repository. That split between the released package and the development tree is worth noticing, because in a library with effects and a moving platform backend, the gap between the two can matter.

The interactive route is the fastest way to see the model. Try out the examples interactively by running utop in the shell, then load the library and open the module:

```ocaml
# #require "eio_main";;
# open Eio.Std;;
```

A program is a function of a flow, and the run function starts the event loop:

```ocaml
let main out =
  Eio.Flow.copy_string "Hello, world!\n" out
```

```ocaml
# Eio_main.run @@ fun env ->
  main (Eio.Stdenv.stdout env);;
Hello, world!
- : unit = ()
```

Eio_main.run automatically calls the appropriate run function for your platform, for example on Linux this will call Eio_linux.run, and for non-portable code you can use the platform-specific library directly. The same example can also be built using dune, in the examples/hello directory.

The two arguments in that snippet carry the design. The env argument represents the standard environment of a Unix process, allowing it to interact with the outside world, and the type of the main function tells us that this program only interacts via the out flow. A program will typically start by extracting from env whatever things the program will need and then calling main with them.

## Capabilities as the testability story, not just a security story

The design note on capabilities is one of the more interesting documents in the OCaml ecosystem, and Eio uses it for testability as much as for safety.

The testable consequence is stated in the testing section. Because external resources are provided to main as arguments, we can easily replace them with mocks for testing. Instead of giving main the real standard output, you can have it write to a buffer:

```ocaml
# Eio_main.run @@ fun _env ->
  let buffer = Buffer.create 20 in
  main (Eio.Flow.buffer_sink buffer);
  traceln "Main would print %S" (Buffer.contents buffer);;
+Main would print "Hello, world!\n"
- : unit = ()
```

That is a real design payoff rather than a testing afterthought. The same main function that talks to a socket in production talks to a buffer in a test, with no conditional compilation and no mock library rewriting your program's logic. The type of the main function is the test seam.

Eio.traceln provides convenient printf-style debugging without requiring you to plumb stderr through your code. It uses the Format module, so the extended formatting directives work there too. The Eio_mock library provides some convenient pre-built mocks for the same purpose.

The capabilities idea underneath is the same one that the examples/capsicum directory in the tree points at, with the capsicum capability mode of BSD as the reference. The philosophy is that a program is handed the specific resources it needs rather than the whole system, and in a language with modules and first-class functions, that restriction can be expressed in the type system rather than bolted on at runtime.

That is also why the env argument exists. It is not a convenience, it is the boundary.

## Concurrency primitives: promises, streams, switches and the race combinator

The synchronisation section of the readme covers a lot of ground, and the tools fall into a few families.

Promises are the future type, with a concurrent cache given as the worked example. Streams are the push-based counterpart, with a worker pool example. Mutexes and semaphores are there, and conditions are there, with a signal handler example. The readme also has a Design Note: Determinism section, which is a good sign for a concurrency library, since deterministic scheduling is what makes a concurrency bug reproducible.

Two of the Eio-specific combinators deserve attention because they are not standard in other libraries. Switches are a scoping construct for resources, and the best practices section has a section on switches as well, which means they are meant to be used systematically rather than occasionally. The pattern of acquiring a resource, using it, and having it released deterministically is exactly the thing that monadic IO libraries force you to write by hand, and switches do it in the language's own syntax.

Racing is the other one. Running several operations concurrently and taking the first result, cancelling the others, is the operation you need constantly in a network client, and the cancellation section of the readme shows that this is integrated rather than bolted on.

Cancellation is treated as a first class concern, which follows from the same design. With effects, the runtime knows about the continuation it is running, so a cancel switch can raise into a waiting operation and unwind it through a real stack, and the backtrace is the real backtrace.

The multicore support is split into a domain manager and an executor pool, which is the standard OCaml 5 arrangement: domains are the parallelism, the domain manager owns them, and the executor pool is where the work runs.

## The testing and tooling surface, from the Makefile and Dockerfile

The build files show how seriously the project treats its own infrastructure, and one of them contains a decision that is worth copying.

The Makefile has an all target that runs dune build @runtest @all, a docs target that builds the API documentation with odoc for all five libraries, bench and stress targets, and separate test targets per backend. The test_posix target sets EIO_BACKEND=posix before running the test suite, which tells you the same tests run against the different backends and that the backend choice is switchable at run time. There is a dscheck target that runs several test_condition, test_rcfd, test_sync, test_semaphore and test_cells executables, and a stress target for the process, semaphore and release stress programs.

The Dockerfile is worth reading twice. It starts from an opam image with a Debian 13 and OCaml 5.5 base, and it pins the opam repository to a specific commit, with a comment saying to ensure the opam-repository is up-to-date. It installs utop and fmt, and then does something deliberate with the opam files: it copies only the .opam files first to help with caching, pins the package with a development version, installs the dependencies only, and installs uring, iomux and yojson, which are the io_uring and JSON dependencies. Only then does it copy the source and install eio_main.

That ordering is a build cache optimisation, and the comment says so. But it also has a second effect: the dependencies are resolved against the opam files before the source is copied, so a source-only change does not invalidate the expensive dependency layer.

There is a bench.Dockerfile and a bench directory, a fuzz directory, a stress directory, and a Dockerfile target in the Makefile that builds an image tagged eio. The tree also carries lib_eio, lib_eio_linux, lib_eio_posix, lib_eio_windows and lib_main, matching the package split.

## Where Eio is the wrong choice, and what the fallbacks cost

There are boundaries, and the project is upfront about several of them.

The first is the OCaml version. You will need OCaml 5.2.0 or later, and the effects that make Eio possible only exist in OCaml 5. That is not a version bump, it is a different language, and a codebase on OCaml 4 cannot adopt Eio without migrating the compiler first. The migration is the project you would have to budget, not the library adoption.

The second is Windows, where Eio_windows is marked incomplete with a help wanted link. If your deployment target is Windows, you are in the position of needing to finish the backend, and the readme is not claiming otherwise.

The third is feature gaps, and here the project gives you an explicit route. You can always fall back to using Lwt libraries to provide missing features if necessary, and the readme notes it is possible to use Eio and Lwt libraries together. The Integrations section lists Async, Lwt, Unix and System Threads, Domainslib and kcas. So the interop story is good, and the honest cost is that a mixed codebase has two concurrency models in it, and every developer on the team has to know which one a given function uses.

The fourth is the io_uring backend itself. It is the fastest path and the most interesting one, but ring buffer systems carry their own configuration and kernel version requirements that a generic API cannot fully hide. If you deploy to a mix of kernel versions, you should verify the backend on the oldest one you support.

The fifth is the licence. The repository is recorded as a custom licence that GitHub cannot classify, with the terms in the LICENSE file, and the tree carries a LICENSE.md rather than a standard file name. If you need to reason about redistribution and linking obligations, read that file rather than assume a permissive grant, and note that the CHANGES.md is where the version history lives since the tree has no CHANGELOG.

The repository is not archived and the last push was on 2026-10-16, with releases v1.4 on 2026-07-23, v1.5 on 2026-08-19 and v1.6 on 2026-09-21, so the pace is roughly monthly.

## Conclusion

Adopt Eio if you are writing OCaml 5 and want concurrent code that reads like ordinary code, with backtraces that work and no monad plumbing in your call sites. Skip it if your codebase is on OCaml 4, or if the missing pieces matter to you, since the Windows backend is marked incomplete and MQTT-style and other features are not in the core API. Verify first that your OCaml version is 5.2.0 or later, that the backend covering your platform is the one you expect, since Eio_main picks it for you, and that the capability model matches how much of the system you want your code to be able to reach.

## FAQ

### What is Eio and what problem does it solve?

Eio is an effects-based direct-style IO stack for OCaml 5. It provides concurrency primitives and a high-level cross-platform OS API so you can read and write files, make network connections or run CPU-intensive work concurrently, without the monadic interface that Lwt and Async require.

### How do I install OCaml 5 and Eio?

You need OCaml 5.2.0 or later. Install it with opam 2.1 or later using opam switch create 5.5.0, then install Eio with opam install eio_main utop. You can instead build the included Dockerfile, and HACKING.md covers installing the latest unreleased development version.

### Which platforms does Eio support?

Eio_main selects a backend for your platform: Eio_posix for POSIX-type systems and Eio_linux for the Linux io_uring backend. Eio_js runs Eio code in the browser via js_of_ocaml, and Eio_windows exists for Windows but is marked incomplete with help wanted.

### How do I test code written with Eio?

Because external resources are provided to main as arguments, they can easily be replaced with mocks. You can pass Eio.Flow.buffer_sink instead of the real standard output and assert on the buffer contents, and the Eio_mock library provides pre-built mocks for the same purpose.

### What concurrency primitives does Eio provide?

Promises, streams, mutexes and semaphores, conditions, switches for resource scoping, and a race combinator, plus cancellation as a first class concern. Multicore support is split into a domain manager and an executor pool on top of OCaml 5 domains.

### Can I use Eio alongside Lwt or Async?

Yes. The readme states it is possible to use Eio and Lwt libraries together, and that you can always fall back to using Lwt libraries to provide missing features. The Integrations section also covers Async, Unix and System Threads, Domainslib and kcas.

## Sources

- [Issues](https://github.com/ocaml-multicore/eio/issues)
- [ocaml-multicore/eio on GitHub](https://github.com/ocaml-multicore/eio)
- [README](https://github.com/ocaml-multicore/eio/blob/main/README.md)
- [Releases](https://github.com/ocaml-multicore/eio/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ocaml-multicore-eio
