← Back to all field notes
Languages & execution

It's OCaml's Time

For decades the programming language landscape has felt like a series of compromises. Want the safety of strong static typing? Enjoy your slow-compiling borrow checker or your verbose enterprise boilerplate. Want fast development and high velocity? Prepare for the runtime chaos of dynamic languages and the recurring dread of “undefined is not a function.” Want modern concurrency? Welcome to the split-ecosystem nightmare of coloured functions.

Quietly, in the background, a language long admired from a distance has been sharpening its blades. OCaml shows up in compiler courses, in the credits of famous proof assistants, and, most famously, as the house language of Jane Street, the one trading firm that bet its business on it. Everyone agrees it is elegant. Almost nobody reaches for it when starting a new service. That gap between respect and adoption has lasted decades.

OCaml 5 closes it. The gap is closing not because of hype but because the one feature that was missing has arrived, and several features that were always present have become more valuable. It is OCaml’s time. Here is why.

The thing that was missing has arrived

For years the real objection to OCaml was not the type system or the syntax or the tooling. It was concurrency. OCaml had a runtime lock, so a beautiful, fast, statically typed program could use exactly one core. On a many-core machine that is not a footnote. It disqualifies the language from whole categories of work.

OCaml 5 ended that. The runtime now supports real shared-memory parallelism through domains, and a single OCaml process can saturate the hardware it runs on. This is not a research branch or a fork you must be brave to use. It is the language.

That short clause, “OCaml 5 ended that,” covers one of the most ambitious retrofits in the history of programming languages. Adding parallelism to a mature language is open-heart surgery on the runtime, and the team set themselves a punishing rule while they performed it: no regression. The change could not slow existing single-threaded programs, and code written for OCaml 4 had to keep compiling and running untouched. From the project’s start at Cambridge’s OCaml Labs in 2013 to the release of OCaml 5.0 in December 2022, it took close to a decade to keep that promise. The bill for the whole transformation came to a few percent of sequential speed, where a change this deep usually costs far more.

The hard part was the garbage collector. The old one assumed a single thread of execution and had to be rebuilt so that each domain gets its own private nursery while the cores share one heap through a mostly concurrent collector, leaving ordinary allocation free of cross-core coordination. An early version that moved objects in memory would have broken every C binding in the ecosystem, so the team rebuilt it as a non-moving collector to preserve that compatibility, and went further: they gave the language a memory model that keeps type safety even when threads race on shared data, where most languages hand you undefined behaviour. Concurrency itself rests not on threads and locks but on algebraic effect handlers, a control mechanism the language never had before. All of that is finished. It landed in mainline OCaml in early 2022 and shipped that December, and the decade of work now sits behind the language rather than in front of you. You write direct-style parallel code, and the runtime it took that long to get right simply runs it.

Those algebraic effects are rarer than the parallelism. A production language that ships them is unusual, and rather than copy the threads and locks of the 1990s or the async/await machinery of the 2010s, the OCaml team built concurrency on effect handlers, which give you a structured, composable way to express concurrency, generators, and backtracking without bolting separate syntax onto the language for each one. The clearest payoff shows up in how OCaml does asynchronous I/O.

No function colouring

Write enough async code in other languages and you know the tax. Functions split into two kingdoms: normal functions and async functions, and the async-ness is contagious. The moment one function deep in a call stack must await something, every caller above it must become async too, or you reach for an escape hatch that blocks a thread and feels like cheating. This is function colouring, and it shapes entire codebases. Library authors must pick a colour. Sometimes they ship both and double their surface area.

OCaml 5 sidesteps the problem. Because concurrency rests on effects, a function that performs I/O looks like an ordinary function and its type announces no colour. The Eio library builds on this directly: you write what looks like plain, direct, blocking code, and the scheduler underneath handles concurrency through effects. No async keyword climbs your call graph. No red functions and blue functions. A function is a function.

The difference is concrete. Here is the way OCaml used to do concurrency, with the Lwt library and its monadic plumbing:

(* Old OCaml: Lwt. Concurrency leaks into the types and the syntax. *)
let fetch_user_score id =
  Db.find_user id >>= fun user ->
  Api.get_score user.name >>= fun score ->
  Lwt.return (user, score)

Every step chains through >>=, the result is a 'a Lwt.t rather than a plain value, and the colour sits right there in the type. A caller cannot use this function without joining the Lwt world itself. Other ecosystems carry the same tax:

// JavaScript: the async keyword is contagious upward.
async function fetchUserScore(id) {
  const user = await db.findUser(id);
  const score = await api.getScore(user.name);
  return [user, score];
}
// Rust: async fn, .await, and the function is now a different color.
async fn fetch_user_score(id: UserId) -> (User, Score) {
    let user = db.find_user(id).await;
    let score = api.get_score(&user.name).await;
    (user, score)
}

The same logic in OCaml 5 with Eio:

(* OCaml 5 + Eio: direct style. No special return type, no await. *)
let fetch_user_score env id =
  let user = Db.find_user env id in
  let score = Api.get_score env user.name in
  (user, score)

No >>=, no await, no async annotation. The return type is (user * score), an ordinary tuple rather than a wrapped one. This function is the same colour as a function that performs no I/O at all. The scheduler still runs other fibers while these calls are in flight; the effect handler suspends and resumes the fiber underneath. Concurrency stays explicit when you want it, still without changing any types:

(* Run both fetches at once; plain values still come back. *)
let fetch_two env id1 id2 =
  Eio.Fiber.pair
    (fun () -> fetch_user_score env id1)
    (fun () -> fetch_user_score env id2)

This matters more than it sounds, because your concurrency model stops leaking into every type signature and every API decision. Code composes the way code should compose.

The old virtues stayed

The excitement over OCaml 5 obscures an easy thing to miss: while the language fixed its one real weakness, it kept everything that earned it admiration in the first place.

It is still straightforward, solid ML, and pragmatic where Haskell is academic. It is functional first, but it allows controlled mutability and imperative code when performance or local logic calls for it. Algebraic data types and exhaustive pattern matching keep the compiler beside you, flagging the case you forgot. Type inference gives you that safety without writing the types; you get the readability of Python with the discipline of a strict static checker. The option type means no null lurks in your data. None of this is exotic. It is the boring, proven core of the language, and boring and proven is exactly what you want under a system you must maintain for years.

The type definition itself does a remarkable amount of work:

(* The states a payment can be in, and the data each state carries. *)
type payment =
  | Pending of { amount : Money.t }
  | Authorized of { amount : Money.t; auth_code : string }
  | Captured of { amount : Money.t; auth_code : string; captured_at : Ptime.t }
  | Refunded of { original : Money.t; refunded_at : Ptime.t }

let describe (p : payment) =
  match p with
  | Pending { amount } -> Fmt.str "awaiting auth: %a" Money.pp amount
  | Authorized { auth_code; _ } -> Fmt.str "authorized (%s)" auth_code
  | Captured { captured_at; _ } -> Fmt.str "captured at %a" Ptime.pp captured_at
  | Refunded { refunded_at; _ } -> Fmt.str "refunded at %a" Ptime.pp refunded_at

An auth_code exists only once a payment reaches Authorized. You cannot construct a Pending payment carrying one, and you cannot read one off a payment that has not reached that state. Elsewhere the usual approach is a single struct with a nullable auth_code, a nullable captured_at, and a status string, where every reader must remember which fields mean something in which status and nothing stops a mistake. In OCaml the illegal combinations do not typecheck. Add a Disputed case later and the compiler points at every match that no longer covers all cases. Your to-do list for the change writes itself.

It is still fast, and it still compiles fast. OCaml produces efficient native code that competes with the serious systems languages, yet the compiler stays famously quick. If you are tired of waiting minutes for Rust or Scala, OCaml will feel like a superpower. That combination is rarer than it should be. Many languages give you runtime performance only after you pay for it with build times that wreck your feedback loop. OCaml gives you a tight edit-compile-run cycle and a fast binary at the end. Its predictable generational garbage collector yields low latency and high throughput; among garbage-collected languages it trades blows with Go and Java while offering a far stronger type system.

Functors, hexagonal architecture, and DDD

There is a deeper reason OCaml deserves a second look, and it concerns how teams have come to structure software.

The industry has spent a decade rediscovering that the valuable, hard-won part of a system is its domain logic, and that this logic should not tangle with databases, web frameworks, or message queues. Hexagonal architecture, also called ports and adapters, states the principle: the domain defines abstract ports, and infrastructure provides concrete adapters that plug into them. Domain-driven design pushes the same idea, insisting that the model stay clean and protected from infrastructural concerns.

OCaml’s module system is one of the best tools ever built for this. A signature is a port, an abstract contract describing what a capability must provide. A module implementing that signature is an adapter. A functor, a module parameterised by other modules, lets you write your whole domain layer against abstract signatures and then instantiate it with whichever adapter you want: an in-memory store for tests, a real database in production. Most languages need a bulky dependency-injection framework or runtime reflection for this. OCaml does it at compile time, with zero runtime overhead.

Here is the whole shape of it in miniature. First the port, a signature describing what a repository must provide and nothing about how:

(* domain/order_repo.ml -- the PORT, referenced elsewhere as Order_repo.S.
   A pure contract: domain operations only, nothing about storage. *)
module type S = sig
  type t
  val find : t -> Order_id.t -> Order.t option
  val save : t -> Order.t -> unit
end

The domain logic is a functor. It takes any module satisfying Order_repo.S and builds the service against it, knowing nothing about databases:

(* domain/order_service.ml -- domain logic, written against the port alone. *)
module Make (Repo : Order_repo.S) = struct
  type error = Duplicate of Order_id.t

  let place repo (order : Order.t) =
    match Repo.find repo order.id with
    | Some _ -> Error (Duplicate order.id)
    | None   -> Repo.save repo order; Ok order
end

Then the adapters, interchangeable implementations of the same port. An in-memory one for tests:

(* An in-memory ADAPTER for tests. How a repo gets built (allocating the
   table, opening a connection) stays an infra detail; the port only ever
   promises find and save. *)
module In_memory : Order_repo.S = struct
  type t = (Order_id.t, Order.t) Hashtbl.t
  let find tbl id = Hashtbl.find_opt tbl id
  let save tbl (ord : Order.t) = Hashtbl.replace tbl ord.id ord
end

And a real one backed by a database, with the same signature:

(* A Postgres ADAPTER for production. Same port, different machinery. *)
module Postgres : Order_repo.S = struct
  type t = Db.connection
  let find conn id = Db.query_one conn (Queries.find_order id)
  let save conn (ord : Order.t) = Db.exec conn (Queries.upsert_order ord)
end

Wiring it together takes one line, the only place where the domain meets infrastructure:

module Service_for_tests = Order_service.Make (In_memory)
module Service_for_prod  = Order_service.Make (Postgres)

The boundary between domain and infrastructure becomes something the type system enforces, not a convention you hope everyone respects. Order_service cannot reach into a database, because it was only ever handed the Order_repo.S signature, and the compiler would reject any attempt. Conformance is checked where each adapter is wired into the functor, so an adapter that drifts from the port fails to compile at the seam, not in production. For anyone doing DDD seriously, that is close to ideal. The algebraic data types help here too: a well-designed OCaml type makes illegal states unrepresentable, the quiet ambition behind much of domain modelling.

What the community is building

Eio is the anchor: the direct-style I/O library the earlier sections lean on, built by the OCaml Multicore team and now past the experimental stage. Docker is migrating Docker for Desktop to direct-style code on Eio, and Jane Street has driven its own production switch to the multicore runtime. When the largest OCaml codebase on earth and a flagship desktop application both move to OCaml 5, the runtime is not a gamble.

The most striking project for anyone who has envied Erlang is Riot, an actor-model multicore scheduler that brings BEAM-style concurrency to OCaml: lightweight processes, no shared state, typed message-passing, and the parts of the Erlang model that earn their keep, supervision trees, process links and monitors, selective receive, and “let it crash” resilience.

(* A Riot process: spawn it, send it a typed message. *)
open Riot

type Message.t += Hello of string

let () =
  Riot.run @@ fun () ->
  let pid =
    spawn (fun () ->
      match receive () with
      | Hello name -> Logger.info (fun f -> f "hello, %s" name)
      | _ -> ())
  in
  send pid (Hello "world")

The remarkable part is that Riot needs no virtual machine. BEAM is an entire runtime built over decades; Riot reaches much of the same ground as an ordinary OCaml library, because effects supply the suspendable processes and domains spread them across cores. To be fair, it is a scheduler, not a replacement for Erlang/OTP: no distributed clustering across nodes, no hot code reloading, cooperative rather than preemptive scheduling, and none of the thirty-year OTP library corpus. But most systems that reach for Erlang never use clustering or hot reloading. They want lightweight processes, isolation, and supervision on one node, and that is exactly the ground Riot covers. That one person could build it as a library is itself the argument.

Underneath sits the steady platform work. OCaml 5.3 added dedicated syntax for effect handlers; 5.4, in early 2026, brought immutable arrays and atomic record fields, the latter built for lock-free data structures. opam 2.5 made updates substantially faster, dune keeps its rapid cadence, and editor support through Merlin and OCaml-LSP keeps improving. None of it is glamorous. All of it is the difference between a language that is interesting and a language you can ship.

And the frontend, too

A backend language is only half a system, and the obvious objection is the browser. The answer is better than most people expect.

OCaml has compiled to JavaScript for over a decade through Js_of_ocaml, which translates compiled bytecode, runtime and all, and the newer wasm_of_ocaml, which targets WebAssembly. But for most frontend work the interesting route is Melange, which compiles OCaml directly to readable JavaScript, one module per file, with the clean output and close interop a frontend developer expects. It uses the ordinary OCaml toolchain rather than working around it: install with opam, build with dune, edit with the same Merlin and OCaml-LSP stack. It reached 1.0 and runs in production, notably at Ahrefs. For people who recoil at OCaml’s syntax, Reason offers a JavaScript-flavored skin over the same compiler, and ReasonReact gives idiomatic React bindings: hooks, JSX, and a type checker covering your props and state. The sibling project ReScript took the opposite path, breaking toward the JavaScript world with its own syntax; it is the better pick for a pure JavaScript team that wants nothing to do with OCaml.

The real prize is not that OCaml reaches the browser. It is that the same OCaml runs on both ends. With Dune’s virtual libraries you share real code between server and client: domain types, validation rules, business logic, written once and compiled natively on the backend and to JavaScript on the frontend. The payment type from earlier, illegal states and all, is the same type in both places. On the server side, Dream is the most approachable framework today, unopinionated and easy to start with, and it already supports clients compiled through Melange. It still runs on the older Lwt, but the next generation is taking shape: cohttp-eio already ships a multicore, direct-style HTTP server, experimental frameworks like Tapak on Eio and Robur’s Vif on its own Miou scheduler have appeared, I am building my own on top of Piaf, and the long-running Ocsigen stack is moving to Eio. The direct-style web framework that needs no await and no monad is close.

Full-stack TypeScript shares code too, but as one weakly typed language across both tiers. Full-stack OCaml shares it as a rigorously typed one, and the client/server boundary, usually where types are lost in translation and rebuilt by hand, becomes one more thing the compiler checks.

The models can write it now

Adopting a less-mainstream language used to mean accepting a thin surrounding layer of examples, answers, and tacit knowledge. That friction has dropped sharply. Today’s flagship AI models write OCaml well: idiomatic code, a working grasp of the module system, clear explanations of a functor or a type error.

The fit is no accident. OCaml is highly structured, its syntax is compact, and it leans hard on type safety, so a model can reason about pure functions and strong types with precision, and the compiler’s strictness validates the result. The model proposes, the type checker disposes. A team curious about OCaml no longer chooses between a language they love and a language their tools already know. The assistant in the editor knows OCaml. The cost of the road less traveled is the lowest it has ever been.

The small ecosystem, reconsidered

The standard argument for Node or Python is the ecosystem: a package for everything, one install away. It is a real advantage. The other half is worth naming too.

A large ecosystem is a large attack surface. The npm registry has become a recurring source of supply-chain incidents: typosquatted packages, hijacked maintainer accounts, malicious post-install scripts, dependency trees so deep that one transitive package nobody chose can compromise a build. A package for everything means a trust relationship with everyone, hundreds of authors you will never review, and a sprawl of abandoned and low-quality libraries you must wade through and keep patched.

A smaller ecosystem is not a pure deficit, then. It is a different trade. OCaml’s libraries skew fewer, more careful, more stable, and the dependency trees stay shallow enough that you can know what you depend on.

And here the argument connects to the previous one. A thin ecosystem used to be disqualifying because writing the missing piece yourself was slow. That cost has fallen through the floor. If you are willing to write the code your problem needs, and with a capable model beside you there is little reason not to be, the calculus inverts: you write a focused library that does exactly what you need, that you fully understand, that drags in no dependency you did not choose, and that nobody can hijack out from under you.

This is not hypothetical. Working this way, I wrote an Eio-native HTTP layer in a day, a durable workflow engine in another, and a reference hexagonal/DDD application tying them together in a third. None pulls in a sprawling dependency tree, because none needed to. A few years ago “just write your own HTTP layer” was a punchline. Now it is a Tuesday. The point is not to rewrite the world. It is that the threshold at which depending on a stranger’s package beats writing it yourself has moved a long way, and a curated, smaller ecosystem starts to look less like a gap and more like a clean foundation.

What still needs work

None of this means OCaml is finished, and an essay that only sells is not worth trusting. The roughest edge is cultural rather than technical: too many libraries are documented thinly or not at all, too many are single-maintainer projects with a bus factor of one, and opam carries its share of half-finished or quietly abandoned packages, sometimes two or three solving the same problem with no signal of which is alive. The shallow-dependency argument from earlier cuts both ways. A small ecosystem is easier to audit, but you do have to audit it, because the package page rarely will. This is the ordinary friction of a language on the way up, and it tends to mend as more companies depend on the language and fund the unglamorous work. But it is real, and worth knowing before you start.

Not just for Jane Street

OCaml has long carried a reputation as the house language of one famous trading firm, and the association, a point of pride, has quietly hurt it. It made OCaml sound like a specialist instrument: brilliant, yes, but for those people, with their problems, doing something the rest of us do not do.

The framing was always too narrow, and OCaml 5 makes it indefensible. A language with real multicore parallelism, color-free concurrency, fast compiles, native-code performance, a world-class module system for clean architecture, a curated and shallow dependency surface, and strong AI tooling support is not a niche instrument. That describes a general-purpose language, one you would want for ordinary backend services, command-line tools, data infrastructure, and domain-heavy business systems, the bulk of what most of us build.

The era of choosing between performance, developer velocity, and type safety is over. The one weakness that truly disqualified OCaml is gone, and what remains is the ordinary friction of a language on the way up, not the way out. What remains is a language that was always quietly excellent and is now, at last, equipped for ordinary work. The respect was always there. It is time to let it become adoption.

It’s OCaml’s time.

// Context & author

You are reading Field Notes by Auxil

Auxil is an independent software systems consultancy and active product factory operated by veteran software practitioner Tim Farland alongside a vetted peer network of senior specialists. Based on Waiheke Island, Auckland, we design, build, and audit high-stakes SaaS systems and production-grade AI pipelines globally.

Explore →

Tim Farland

Operator / Architect / Engineer
// Contact

Let's discuss your project

If you are looking for a reliable, competent, efficient Principal Architect or Engineer for scoped, delivery-focused contracting or advisory, reach out.

Waiheke Island, Auckland · Available for remote or CBD hybrid engagements