Typed Concurrency in Gleam: Safe Processes and Supervisors on the BEAM
Gleam’s process API brings typed message passing and OTP supervision to the BEAM. Learn how to spawn safe processes, link them, and build a counter service that restarts on failure—all while catching message mismatches at compile time.
26 Nov 2025, 12:54 UTC

Why Typed Concurrency Matters in the BEAM
When building distributed services on Erlang’s BEAM, the classic model is to spawn lightweight processes and exchange untyped messages. That model works well, but it also opens a door to subtle bugs: a process can receive a message it doesn’t understand, leading to runtime crashes or silent failures. Gleam addresses this by providing a gleam/process module that enforces type safety at compile time, making the message contract explicit and eliminating a whole class of runtime errors.
Spawning a Process with process.spawn
The API is intentionally simple. A process is created by calling process.spawn(fn) where fn is a function that returns Nil. The function runs in a separate Erlang process, and any uncaught exception causes that process to terminate. The signature looks like:
pub fn spawn(counter: Int) : Result(Pid, Error) {
process.spawn(fn() -> Nil {
loop(counter)
})
}
Running this function requires the project to compile to the Erlang target. The JavaScript target does not expose OTP primitives, so this code will be unavailable there.
Typed Message Passing
Gleam’s process.send and process.receive functions are type‑checked. You define a message union type and use pattern matching in the receiving process. For example:
type CounterMsg is
| Increment
| Get(Pid)
fn loop(count: Int) {
process.receive::(fn(msg) {
case msg {
Increment =>
loop(count + 1),
Get(reply_to) =>
process.send(reply_to, count),
loop(count)
}
})
}
Because the compiler knows the exact shape of CounterMsg, sending a message of any other type will fail to compile, preventing accidental message mismatches.
Linking and Supervision
Processes can be linked to supervise each other. In Gleam you can create a supervisor that restarts a child process when it crashes. The gleam/otp module provides a simple wrapper around Erlang’s OTP behaviour. A minimal supervisor looks like:
pub fn start_counter_supervisor() {
let child_spec = {"counter", fn() => spawn(0)}
let supervisor = otp.supervisor([child_spec])
process.link(supervisor.pid)
}
When the counter process exits due to an unhandled exception, the supervisor automatically restarts it. This pattern mirrors classic Erlang OTP behaviour but with type safety baked in.
Concrete Worked Example: A Counter Service
- Project Setup: Create a new Gleam project with
gleam new counter_demoand add the code tosrc/counter_demo.gleam. - Compile to Erlang: Run
gleam erlangto generate BEAM bytecode. - Run the Demo: Execute
gleam run. The main module should start the supervisor and spawn the counter. - Interact: In a separate terminal, start an Erlang shell with
iex -S mixand send messages:pid = :counter_demo.Counter.start_counter_supervisor() :counter_demo.Counter.send(pid, :counter_demo.CounterMsg.Increment) :counter_demo.Counter.send(pid, :counter_demo.CounterMsg.Get(self())) - Verify Supervision: Kill the counter process with
Process.exit(pid, :kill)and observe that the supervisor restarts it, logging the restart.
To confirm type safety, try to send a string instead of a CounterMsg and watch the compiler reject the code.
Trade‑offs and Limitations
- Target Restriction: The
gleam/processAPI is only available when compiling to Erlang. Projects targeting JavaScript or WebAssembly cannot use these primitives. - Boilerplate: Defining a message union type and matching on it adds a few lines of code compared to raw Erlang
receiveblocks. For small scripts, this overhead might feel unnecessary. - Runtime Overhead: Typed message passing introduces a small compile‑time cost due to type checking, but at runtime the BEAM executes the same lightweight process model as untyped Erlang.
Actionable Takeaways
If you’re building a service that needs reliable, concurrent state management on the BEAM, consider using Gleam’s process module. It gives you:
- Compile‑time guarantees that only the intended messages reach a process.
- Automatic crash handling via OTP supervisors.
- A concise API that hides the boilerplate of raw Erlang pattern matching.
Start by creating a small counter or queue service, experiment with the message types, and then scale up to more complex workflows. The type‑safe process model can reduce the number of runtime bugs and make your concurrent code easier to reason about.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.