Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

OCaml  Module 12 – Effects, concurrency and parallelism (OCaml 5)

Concurrent I/O with Eio and Lwt

Run concurrent I/O in OCaml 5 with Eio_main.run, switches and fibers; bound a request with a timeout, cap buffered input, and read the Lwt promise style.

  • Advanced
  • 15 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: Fibers wait together, and a switch waits for its fibers · Why direct-style reads like ordinary code · Practise fibers, switches and direct style. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Run Eio programs with Eio_main.run, Eio.Switch.run and Eio.Fiber.both
  • Compare serial waits and Fiber.both using a mock clock
  • Limit an operation with Eio.Time.with_timeout and a Buf_read line cap
  • Read Lwt promise code with let*, Lwt.both and Lwt_main.run

Before you start

What you’ll learn · Before you start

What you will learn

  • Run Eio programs with Eio_main.run, Eio.Switch.run and Eio.Fiber.both
  • Compare serial waits and Fiber.both using a mock clock
  • Limit an operation with Eio.Time.with_timeout and a Buf_read line cap
  • Read Lwt promise code with let*, Lwt.both and Lwt_main.run

Before you start

On this page

A program that waits on two slow operations one after the other takes the sum of the two waits; a program that waits on them together takes the longer one. On a mock clock, the recorded run below took 3.5 seconds for two sleeps one after the other and 2.0 seconds for the same two sleeps under Fiber.both.

structured concurrency: A way of organising concurrent fibers so that a parent scope owns every child it starts: the scope waits for every child to finish or be cancelled before it returns. In Eio, Eio.Switch.run opens such a scope. is how Eio, the direct-style I/O library for OCaml 5, lets a program own that overlap without threads or callbacks: every fiber: A lightweight thread of execution inside one domain: only one fiber of a domain runs at a time, until it waits for input, output or a timer, and then the next ready fiber runs. runs inside a scope that waits for it. You will run two waits together, attach fibers to a switch that waits for them, and read the results in direct style. A budget with cancellation: Stopping a fiber or a promise before it finishes, for example because a sibling failed or a time budget ran out; in Eio, Fiber.both and a switch cancel their other fibers when one of them raises. and a cap on buffered input build on that, and Lwt’s monadic concurrency: A style in which a function that waits returns a wrapped value such as 'a Lwt.t, and callers chain further work with bind (let*). Every waiting call shows in the types, and every caller of one returns a promise too. is set beside it.

Fibers wait together, and a switch waits for its fibers

Concurrent I/O with Eio starts at Eio_main.run, which picks an event loop: The scheduler at the heart of a concurrent-I/O library: it keeps the fibers or promises that are ready, runs one until it has to wait, and resumes it when the operating system reports that its input, output or timer is ready. Eio and Lwt each have one. back end for the platform (eio_posix was installed with it on macOS at version 1.6) and hands your function one env with a clock, a filesystem, a network and so on. A fiber is a light thread of execution that Eio switches away from whenever it waits. Eio.Fiber.both f g runs f and g as two fibers and returns when both have finished; if one raises, it cancels the other. Eio.Fiber.pair has the same shape and returns the two results as a pair.

The session below runs two “slow” operations one after the other, then with Fiber.both. It uses Eio_mock.Backend.run_full in place of Eio_main.run: its clock moves only when every fiber is asleep, so the printed times are the same on every run.

fibers.ml: the same two waits run four ways on a mock clock ML · fibers.ml
(* Two "slow" operations on a mock clock: one after the other, then with
   Eio.Fiber.both, then Eio.Fiber.pair, then two fibers forked onto a switch.
   Eio_mock.Backend.run_full stands in for Eio_main.run: it hands over an env
   whose clock advances only when every fiber is asleep, so the printed times
   are the same on every run. *)

let fetch label clock delay =
  let t0 = Eio.Time.now clock in
  Eio.Time.sleep clock delay;
  let t1 = Eio.Time.now clock in
  Printf.printf "%s: finished at t=%.1fs (waited %.1fs)\n%!" label t1 (t1 -. t0)

let one_after_the_other clock =
  let t0 = Eio.Time.now clock in
  fetch "serial a" clock 1.5;
  fetch "serial b" clock 2.0;
  let t1 = Eio.Time.now clock in
  Printf.printf "serial total: %.1fs\n%!" (t1 -. t0)

(* Fiber.both needs no switch: it returns when both fibers have finished. *)
let together clock =
  let t0 = Eio.Time.now clock in
  Eio.Fiber.both
    (fun () -> fetch "concurrent a" clock 1.5)
    (fun () -> fetch "concurrent b" clock 2.0);
  let t1 = Eio.Time.now clock in
  Printf.printf "concurrent total: %.1fs\n%!" (t1 -. t0)

let pair_result clock =
  let a, b =
    Eio.Fiber.pair
      (fun () -> Eio.Time.sleep clock 0.5; "answer A")
      (fun () -> Eio.Time.sleep clock 0.7; 42)
  in
  Printf.printf "pair: %s and %d\n%!" a b

(* Fiber.fork returns at once; the switch the fibers are attached to waits. *)
let forked clock =
  let t0 = Eio.Time.now clock in
  Eio.Switch.run (fun sw ->
    Eio.Fiber.fork ~sw (fun () -> fetch "forked a" clock 1.5);
    Eio.Fiber.fork ~sw (fun () -> fetch "forked b" clock 2.0);
    print_endline "both forked; Switch.run now waits for them");
  let t1 = Eio.Time.now clock in
  Printf.printf "switch total: %.1fs\n%!" (t1 -. t0)

let () =
  Eio_mock.Backend.run_full @@ fun env ->
  let clock = env#clock in
  one_after_the_other clock;
  print_endline "---";
  together clock;
  print_endline "---";
  pair_result clock;
  print_endline "---";
  forked clock
Serial waits add; Fiber.both, Fiber.pair and a switch take the longer wait Python · fibers_session.py
"""Two slow operations on a mock clock: serial waits add, Fiber.both and
Fiber.pair overlap them, and two fibers forked onto one switch keep
Switch.run waiting until both have finished. The mock clock advances only
when every fiber is asleep, so the printed times are the same on every run.
"""
from session import files, run, write

files("fibers.ml")
write("dune-project", "(lang dune 3.0)\n")
write("dune", "(executable (name fibers) (libraries eio.mock))\n")
run("""
dune exec ./fibers.exe
""")

Output

$ dune exec ./fibers.exe
+mock time is now 1.5
serial a: finished at t=1.5s (waited 1.5s)
+mock time is now 3.5
serial b: finished at t=3.5s (waited 2.0s)
serial total: 3.5s
---
+mock time is now 5
concurrent a: finished at t=5.0s (waited 1.5s)
+mock time is now 5.5
concurrent b: finished at t=5.5s (waited 2.0s)
concurrent total: 2.0s
---
+mock time is now 6
+mock time is now 6.2
pair: answer A and 42
---
both forked; Switch.run now waits for them
+mock time is now 7.7
forked a: finished at t=7.7s (waited 1.5s)
+mock time is now 8.2
forked b: finished at t=8.2s (waited 2.0s)
switch total: 2.0s

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fibers_session.py

Serial fetches of 1.5 s and 2.0 s finished at t=1.5 s and t=3.5 s, a total of 3.5 s. Under Fiber.both both fibers went to sleep at t=3.5 s; the backend printed +mock time is now 5 when the first wait ended and now 5.5 when the second did, a total of 2.0 s, the longer of the two waits. Step through that second run:

Interactive · message by message Two fibers, one mock clock, one wait Follow the two fibers of Fiber.both and the mock clock. The clock moves only when every fiber is asleep, so the printed times are the same on every run.
Two fibers, one mock clock, one wait, step 5 of 5. Messages so far: fiber a to mock clock: sleep 1.5; fiber b to mock clock: sleep 2.0; mock clock to fiber a: wake at 5.0; mock clock to fiber b: wake at 5.5.fiber areturned at 5.0mock clockstarts at 3.5fiber breturned at 5.5sleep 1.5sleep 2.0wake at 5.0wake at 5.5

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Fiber a starts and sleeps. Fiber.both runs fiber a at once. It reads the clock, which the serial run left at 3.5, and calls Eio.Time.sleep for 1.5 s, so it waits until 5.0.
  2. Fiber b starts and sleeps. Fiber b runs next and calls Eio.Time.sleep for 2.0 s, so it waits until 5.5. Both fibers are asleep at the same time.
  3. The clock moves to 5. No fiber is ready, so the mock backend moves the clock to the first due timer and prints +mock time is now 5. Fiber a wakes, prints that it finished at t=5.0s after waiting 1.5s, and returns.
  4. The clock moves to 5.5. Only fiber b is left. The backend prints now 5.5, and b prints that it finished at t=5.5s after waiting 2.0s.
  5. Fiber.both returns. Both fibers have finished, so Fiber.both returns and the program prints concurrent total: 2.0s, against serial total: 3.5s for the same two waits one after the other.

Fiber.pair paired a 0.5 s fiber returning "answer A" with a 0.7 s fiber returning 42 and returned both. The last run opened Eio.Switch.run and attached two fibers to its switch with Eio.Fiber.fork ~sw. fork returns at once, so both forked; Switch.run now waits for them printed first, and Switch.run returned only when both fibers had finished, again after 2.0 s. A switch waits for every fiber attached to it, releases the resources attached to it, and cancels its other fibers when one of them raises.

Key idea

Fiber.both and Fiber.pair return when both of their fibers have finished, and Eio.Switch.run returns when every fiber forked onto its switch has: the longer wait, not the sum, decides the time.

Why direct-style reads like ordinary code

Eio builds its scheduler on top of the OCaml 5 effect handlers of the two earlier lessons of this module. sleep, accept, read and write perform effects that the backend catches, so Buf_read.line r is an ordinary call that returns a string. The type of a function that waits is the type of its answer, as three waiting calls show:

Waits for Eio Lwt
a line Buf_read.line r : string Lwt_io.read_line ic : string Lwt.t
0.05 s Eio.Time.sleep clock 0.05 : unit Lwt_unix.sleep 0.05 : unit Lwt.t
two waits Fiber.both f g : unit Lwt.both p q : ('a * 'b) Lwt.t

Concurrent I/O with Eio therefore reads like the sequential code it replaces.

Three tools build on fibers. Eio.Time.with_timeout gives an operation a budget and cancels it when the budget runs out, returning Error with a `Timeout tag. Buf_read.of_flow ~max_size:N caps the bytes a reader buffers, so one client cannot decide how much memory the server uses. Lwt is the promise library a Dream server or an existing cohttp-lwt client is written with, so reading its let* and Lwt_main.run code is part of working in OCaml today.

Checkpoint

Check: Eio_main, Fiber.both and switches · Why Eio reads like ordinary direct-style code

  1. Question 1 of 3 What does Eio.Switch.run do after its function returns, before Switch.run itself returns?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: Waits for every fiber forked onto the switch to finish, and releases every resource attached to it

    A switch is to fibers and sockets what Fun.protect is to one resource: a scope that waits for cleanup.

  2. Question 2 of 3 In the recorded fibers run, the serial pair of 1.5 s and 2.0 s sleeps took 3.5 s. How long did the same pair take under Eio.Fiber.both, in seconds?

    Type a number.

    A hint

    Which of the two numbers decides the time when the operations overlap?

    Show the answer: Question 2 of 3

    Answer: 2 seconds

    The slower wait, 2.0 s, decides the time when the two run under Fiber.both; the recorded run shows concurrent total: 2.0s.

  3. Question 3 of 3 In an Eio program, what is the return type of a function that reads one line from a socket, Buf_read.line r?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: string

    Eio builds on OCaml 5 effect handlers, so awaiting is an effect the scheduler handles rather than a monadic shape in the type.

Practise fibers, switches and direct style

Every question below is about the fibers above or the types of direct style: predict the wait or the result before you choose.

Practice that adapts to you

Practise this lesson

5 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 5 What does Eio_main.run hand your function?

    Choose one answer.

    Show the answer: Question 1 of 5

    Answer: An env with a clock, a filesystem, a network and other resources the backend provides

    env is the handle through which a program reaches every service of the backend: clock, net, fs, process manager and so on.

  2. Question 2 of 5 Fiber.both runs two fibers that sleep for 0.8 s and 1.3 s on a mock clock. How long does Fiber.both take before it returns, in seconds?

    Type a number.

    A hint

    Fiber.both overlaps the waits; the longer one decides the time.

    Show the answer: Question 2 of 5

    Answer: 1.3 seconds

    The two sleeps overlap, so Fiber.both returns after 1.3 s, not 0.8 + 1.3.

  3. Question 3 of 5 An Eio service has 10,000 open connections, each handled by one fiber under one switch. How are they scheduled on a 10-core machine?

    Choose one answer.

    Show the answer: Question 3 of 5

    Answer: All fibers share one domain and the scheduler runs the one whose effect is ready; cores are added by spawning domains

    Fibers are cheap because they share a domain; parallelism is a separate concern that more domains address.

  4. Question 4 of 5 A function in Eio reads two lines from a socket and returns their concatenation. What is a plausible type for it?

    Choose one answer.

    Show the answer: Question 4 of 5

    Answer: _ Eio.Net.stream_socket -> string

    The whole point of direct style is that the type of the answer is the type the function returns.

  5. Question 5 of 5 A codebase on OCaml 4 uses let* throughout and Lwt_main.run at its root. Which option fits best for new code added inside it?

    Choose one answer.

    Show the answer: Question 5 of 5

    Answer: Keep using Lwt; adding Eio to Lwt code on OCaml 4 is not possible without upgrading to 5

    Which library fits a codebase depends on its OCaml version and its existing event loop; a program on OCaml 4 cannot use Eio at all.

Cancellation, timeouts and an input limit a service lives behind Pro: opens with a Pro, Premium or Ultimate pass

A loopback echo server with a 1,024-byte line cap is written in the same direct style as the mock-clock examples, because Eio hides the scheduler. A handler stops a bad request in three ways: a budget on the clock, a cap on the buffer, and the cancellation of the other fibers when one of them fails.

In this part:

  • A budget on the clock with with-timeout
  • Bounding input length is input validation
  • A loopback echo server, bound to 127.0.0.1 only
  • A slow reader slows the writer

1 diagram · 6 runnable examples (ML, Python) · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Reading Lwt: promises, let-bind and where you still meet it Pro: opens with a Pro, Premium or Ultimate pass

Many OCaml services written before OCaml 5 use Lwt, so a reader meets let*, Lwt.return and Lwt_main.run every time a Dream handler or a cohttp-lwt client turns up in a codebase. The same logic as the Eio examples, written in Lwt’s style, sets the two libraries side by side on facts the recorded runs show.

In this part:

  • A promise is a value that will arrive
  • Monadic concurrency colours every function that waits
  • Failures in Lwt flow through the promise, not through try
  • Promises you forget are still running
  • Choosing between Eio and Lwt for a new or existing service
  • Lwt_eio runs Lwt and Eio code in one domain
  • Key takeaways from the two parts

2 runnable examples (ML, Python) · about 11 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

Warm-up for freshers and 2–5 years: what is an Eio fiber, and how does Eio.Fiber.both differ from spawning two threads?

An Eio fiber is a lightweight unit of execution that Eio’s event loop schedules on top of OCaml 5 effect handlers: calling Eio.Time.sleep, Buf_read.line or Eio.Net.connect performs an effect that suspends the fiber until the backend has an answer, and other fibers run meanwhile. Eio.Fiber.both f g runs two fibers and returns when both have returned, so the wall time is the slower of the two waits, not the sum, and if one raises it cancels the other. Two systhreads of one domain also take turns, since only one of them at a time runs OCaml code, but each is an operating-system thread with its own stack. Fibers are switched by Eio, inside one thread, at the points where they wait.

Three harder questions follow: a service whose latency spikes come from one slow upstream, a line-based server that gets a request with no newline for a megabyte, and a team choosing between Eio and Lwt for a new HTTP service. Pick your first move before opening each:

  1. Mid to senior

    A service that calls one slow upstream has p99 latency 3 s when the upstream slows down, and p50 under 50 ms when it is healthy. The team writes the call under Eio. How would you keep p99 near the SLO without changing the upstream?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A line-based Eio server reads one request a connection with Eio.Buf_read.line, no cap. A client opens a connection and sends 1 MB of bytes with no newline. The process memory climbs. What would you change, and why?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    A team is writing a new HTTP service in OCaml. One engineer wants Eio for direct style; another wants Lwt, because the shared logging library and the client for the next service are both cohttp-lwt. What would you propose?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How long do two slow waits take one after the other, and together?

    One after the other, the sum of the two; together, the longer of the two.

  2. What do Eio.Fiber.both and Fiber.pair do?

    They run two fibers and return when both have finished, so the wall time is the slower wait, not the sum; if one raises, the other is cancelled.

  3. What does Eio.Switch.run guarantee about the fibers forked onto its switch?

    It returns only when every fiber forked with Fiber.fork ~sw has finished, releases what is attached to the switch, and cancels the other fibers when one of them raises.

  4. In direct style, what does a function that waits return, compared with the Lwt version?

    Its answer's own type, string for Buf_read.line, where the Lwt version returns string Lwt.t.

  5. How do Eio fibers differ from two systhreads of one domain?

    Fibers are switched by Eio, inside one thread, at the points where they wait; each systhread is an operating-system thread with its own stack.

Key takeaways

  • Eio_main.run hands a program its env; Eio.Fiber.both and Fiber.pair run two fibers and return when both have finished, so the wall time is the slower wait, not the sum.
  • Eio.Switch.run waits for every fiber forked onto its switch with Fiber.fork ~sw, releases what is attached to it, and cancels its other fibers when one of them raises.
  • In direct style a function that waits returns its answer’s own type, string for Buf_read.line, where the Lwt version returns string Lwt.t.
  • Eio.Time.with_timeout gives an operation a budget, Buf_read.of_flow ~max_size:N caps buffered input, and Lwt’s let* style is what a Dream server or a cohttp-lwt client is written in.

Terms in this lesson

cancellation
Stopping a fiber or a promise before it finishes, for example because a sibling failed or a time budget ran out; in Eio, Fiber.both and a switch cancel their other fibers when one of them raises.
event loop
The scheduler at the heart of a concurrent-I/O library: it keeps the fibers or promises that are ready, runs one until it has to wait, and resumes it when the operating system reports that its input, output or timer is ready. Eio and Lwt each have one.
fiber
A lightweight thread of execution inside one domain: only one fiber of a domain runs at a time, until it waits for input, output or a timer, and then the next ready fiber runs.
input limit
A cap on how many bytes a reader will buffer from untrusted input before raising an error, chosen by the service designer and matched to the longest legitimate value; bounding input length is part of input validation.
monadic concurrency
A style in which a function that waits returns a wrapped value such as 'a Lwt.t, and callers chain further work with bind (let*). Every waiting call shows in the types, and every caller of one returns a promise too.
structured concurrency
A way of organising concurrent fibers so that a parent scope owns every child it starts: the scope waits for every child to finish or be cancelled before it returns. In Eio, Eio.Switch.run opens such a scope.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.