Why Ocsigen is staying on Lwt (for now)

Over 2025 we migrated the whole Ocsigen stack (ocsigenserver, Eliom, ocsigen-toolkit, ocsigen-start) from Lwt to Eio. The migration compiles and the test suite passes. We have nonetheless decided not to release it and to stay on Lwt for now, and we wrote up why.

Two obstacles stopped us:

  • Loss of function coloring. Under Lwt, a _ Lwt.t in a type tells you a call may suspend or do I/O. In a multi-tier framework like Eliom this is crucial: the same shared expression can be a local call on the server and a network round-trip on the client. Without coloring, nothing at the call site tells you where to put a spinner, and reactive combinators (React / map_s) silently lose the guarantee that they are pure.
  • Eio’s handler model vs browser events. On the client, DOM event handlers are called directly by the browser with no Eio handler on the stack, so any perform crashes. The usual workaround (Eio_js.start) defers the body via setTimeout(0), which runs after event propagation and breaks preventDefault / stopPropagation.

This is not an anti-Eio post. Effects are a major achievement of OCaml 5 and we are grateful to the multicore team. We also think the monadic style is genuinely fine (with let* it is comfortable to write), and Lwt_direct is there for those who dislike monads.

Our real worry is unity. Migrating a very large codebase is genuinely hard, and the less well-funded projects on Lwt simply cannot afford it. More broadly, the state of concurrency libraries in OCaml is, in our view, extremely concerning: the community is small and cannot afford a split. If OCaml is to grow it must reach real-world applications, and those users need high-level, interoperable building blocks. Ocsigen has always aimed to provide such tools while fostering the unity of the ecosystem, and we are fully committed to helping solve the problem of concurrency libraries. We would welcome your view on directions that could unblock us, including improving Lwt itself (io_uring, effects, multicore).

Full write-up: Why Ocsigen decided not to switch from Lwt to Eio (for now) — Ocsigen

Feedback, ideas and constructive disagreement very welcome.

Thanks for writing this piece and articulating this concern. I’ve appreciated the move to direct style concurrency in Eio, having struggled with Lwt’s monadic style as a beginner (I’ve come along from there now but haven’t really revisited lwt in detail).

However, as I’ve started to build things with eio on the server side that have more complex concurrency behaviour (particularly with promises and conditions, but also switches), I’ve wondered if the lack of extra types has been a loss of information in the code, and adds an extra source of confusion. It’s very easy to wrap these things up so that the consumer doesn’t see them, even if they’re passing in objects like the switch or env parameter that usually indicate some kind of concurrency behaviour.

Lwt’s PPX actually makes the monadic style look quite direct. In case you haven’t seen the docs: lwt_ppx 6.1.0 (latest) · OCaml Package

These days you don’t really need the PPX, you can just use the provided let operators.

The ppx is useful for try%lwt, match%lwt, while%lwt, etc.

Your post mentions that typed effects could help, though obviously it’s not something that can be experimented with, much less used for real.
Have you tried instating the type discipline yourself in some way?

For instance, a ->{suspend} b is not very different from async -> a -> b, and that’s something that could be experimented with now (what’s worse compared to effects is that you shouldn’t capture the async capability for reuse later, and the typer won’t help you avoid that, at least without oxcaml’s local parameters. Though that strikes me as being an easy invariant to uphold).
I didn’t really understand the second obstable, but val Eio_js.start : (async -> unit) -> unit would be the function that provides a value of type async, much like Eio_main.run is the one place that provides Eio.env, so it seems that, at a minimum, it’d help with knowing where Eio_js.start is needed.

for function colouring i’ve wondered for a while if there should be a best practice recommendation for eio to wrap all asynch functions exposed in a library’s interface to be wrapped in eio’s promises.

it’s like responsible documentation

Worth noting that Lwt_direct.spawn always returns a _ Lwt.t, so it’s still visible in the types despite allowing direct style in the fiber’s body.

Yes, but as long as this is not mandatory, user will be able to use it wrong. Capabilities can be hidden (and, btw, yield does not take a capability in Eio). Ocsigen is meant to be used by Web and pplications developers. We want to guide them by making a lot of errors impossible. Eliom-eio is a huge regression in terms of reliability of the apps they might write.

Makes sense. Ocsigen isn’t using Lwt strictly for concurrency, but also to serve as a DSL separation layer between pieces of the code. If Lwt never existed, you’d have had to create your own monad-based DSL to preserve the properties you need. I think for most concurrency purposes though, direct-mode is preferable.

Thanks for the writeup Vincent! I admit I’m a bit surprised that you embarked on the Eliom port to Eio at all, since the explicit goal of Eio is to be a direct-style IO library that removes IO from types. If Eliom depends on syntactic yield points as part of its multistage compilation semantics, then this’ll never work. The capabilities in Eio are to eliminate ambient authority (for the purposes of mapping to better system calls), and definitely not for concurrency.

Your second point is a general problem with effects-using libraries that require an FFI – even for C – since effects cannot cross a C stack. Js_of_ocaml is forced to go through some gymnastics (the regional CPS etc) to have a reasonable encoding, and wasm is in its usual multiverse of extensions like stack switching in order to get reasonable performance.

Lwt’s strategy of monadic concurrency seems pretty optimal for the way that Eliom is currently designed. I could see how to build a new multistage web library using effects, but its design would be a big departure from the current architecture of Eliom and so not particularly helpful to your existing userbase. However, you’re very welcome to grab the component libraries like ocaml-uring for Lwt of course, since these have no dependency on Eio at all (nor will they in the future).

This would seem to defeat the point of Eio if we end up with wrapped function calls everywhere. If you’re calling the library in question from another Eio library, there’s no need to use promises most of the time. If you’re in a foreign runloop, then that should wrap the overall Eio runloop carefully (for the same reason as the above FFI discussion).

I’m curious what you’d like here, since there’s quite a big field of tracing tools cropping up these days. Eio’s structured concurrency is intended to be dynamic, but tracing through switches works pretty well (see @talex5 on performance optimisation or @patricoferris on using meio to debug his new shell). I’ve been wondering about higher level ways to view this concurrency graph, so your thoughts on the Eio issue tracker are most welcome.

This wasn’t meant as a complaint, but more a sense of uncertainty about type-level information. My context is that I’ve been using LLMs to work with structured concurrency using eio, and I speculate that the monadic type annotation on concurrent code may help reduce errors in their output. I have some ideas but I’m yet to properly test them.

The resources you linked to are very helpful and I think I need to experiment with at least eio-trace a bit more. Threading its output with other tools could be very powerful for visualisation and debugging, but I don’t have the complexity of use cases to validate that idea.

(You’ve also pointed me to a bunch of things like Patrick’s attempt to implement a shell in ocaml which I was unaware of, something I’ve wondered about as a way to better underpin LLM agent tool call actions, both as a way of recording what happened in a session, as well as helping them validate the output of their intentions, especially combined with a virtualised filesystem)