Language Atlas (mini-courses for more languages) Module 10 – Gleam on the BEAM and in JavaScript
Gleam processes, actors and FFI
Spawn BEAM processes that talk through typed subjects, build a gleam_otp actor, restart it under a supervisor, and call Erlang and JavaScript code safely.
What you will learn
- Start processes that send messages through a subject, and give every receive a timeout
- Build an actor with gleam_otp that keeps state, answers calls and never sells a unit twice
- Run an actor under a supervisor, and predict which state survives its restart
- Call Erlang and JavaScript code through @external functions, and explain what the compiler cannot check
Before you start
What you’ll learn · Before you start
What you will learn
- Start processes that send messages through a subject, and give every receive a timeout
- Build an actor with gleam_otp that keeps state, answers calls and never sells a unit twice
- Run an actor under a supervisor, and predict which state survives its restart
- Call Erlang and JavaScript code through @external functions, and explain what the compiler cannot check
Before you start
On this page
Ten processes asked one Gleam actor for a unit of stock at the same moment, with four units left, and exactly four got one: there was no lock and no transaction, only a process that handles one message at a time. On the BEAM, Gleam programs are built from such processes, and when one crashes, a supervisor starts it again, with its first state rather than its last.
Here you start processes and pass them typed messages, turn one into an actor that keeps stock, crash it under a
supervisor and watch what comes back, and then call Erlang and JavaScript code from Gleam, including a binding whose
type is wrong. Every program ran on Erlang/OTP 29.1.1 with gleam_erlang 1.3.0 and gleam_otp 1.3.0, which work on
the Erlang target only; the last section runs on Node.js 24.21.0 too.
A process runs one function, and a subject carries its messages
Erlang’s reference manual calls its process (BEAM): A lightweight unit of execution on the BEAM, with its own memory and its own mailbox. Processes share nothing and work together by sending each other messages; in Gleam, process.spawn from gleam_erlang starts one. lightweight: small in memory, fast to
create and end, and cheap to schedule. Its guide to concurrent programming adds that they share no data, which is
the reason for the name: each has a message queue, its mailbox, and they work together by sending messages. In
Gleam, process.spawn from gleam_erlang starts one, running the function you give it beside the process that
started it. Messages travel through a subject (Gleam): A value through which processes send messages of one type, such as Subject(Int). It is owned by the process that created it: any process may send to it, and only the owner may receive from it.: the documentation describes it as a value
for sending and receiving messages in a well-typed way. A subject belongs to the process that made it, so any
process may send to it but only that owner may receive. counter.gleam starts five processes, and each sends the
square of its number back to main:
import gleam/erlang/process
import gleam/int
import gleam/io
import gleam/list
import gleam/string
pub fn main() -> Nil {
// A subject belongs to the process that made it: here, main.
let results = process.new_subject()
// Five processes, each sending one message back to main.
list.each([1, 2, 3, 4, 5], fn(n) {
process.spawn(fn() { process.send(results, n * n) })
})
// Five receives, each waiting at most 1000 ms for the next message.
let squares =
list.map([1, 2, 3, 4, 5], fn(_) {
let assert Ok(square) = process.receive(results, within: 1000)
square
})
io.println("sum of the squares: " <> int.to_string(int.sum(squares)))
io.println(
"sorted: "
<> list.sort(squares, int.compare)
|> list.map(int.to_string)
|> string.join(", "),
)
// A sixth receive: nobody sends again, so it gives up after 100 ms.
case process.receive(results, within: 100) {
Ok(_) -> io.println("an unexpected sixth message")
Error(Nil) -> io.println("no sixth message within 100 ms: Error(Nil)")
}
} Step through the run, from the subject to the receive that finds nothing:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A subject. process.new_subject() makes a subject owned by main. Any process may send to it; only main may receive from it.
- Spawn five. list.each starts one process for each n from 1 to 5 with process.spawn. Each runs its own function, beside main, and each closes over results.
- Send the squares. Each process sends n * n to results and ends. The messages land in main's mailbox in whatever order the processes ran, which is why the program sorts before it prints.
- Five receives. main calls process.receive(results, within: 1000) five times. Each call takes one message for this subject, or gives up after 1000 ms with Error(Nil).
- The sum. All five arrived, so main printed sum of the squares: 55 and the sorted list 1, 4, 9, 16, 25.
- A sixth receive. Nobody sends a sixth message, so receive(results, within: 100) returned Error(Nil) after 100 ms: a timeout is a value, not a hang.
from session import run
# Five processes each send one square back to main through main's subject; then a receive that times out.
run(
"""
cd processes
gleam run -m counter
"""
) Output
$ cd processes
$ gleam run -m counter
Downloading packages
Downloaded 4 packages in …s
Compiling gleam_stdlib
Compiling gleam_erlang
Compiling gleam_otp
Compiling gleeunit
Compiling processes
Compiled in …s
Running counter.main
sum of the squares: 55
sorted: 1, 4, 9, 16, 25
no sixth message within 100 ms: Error(Nil)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 processes_session.py
All five messages arrived and main printed 55. The messages land in main’s mailbox: The queue of messages that a BEAM process has received and not yet handled. Messages from one sender arrive in the order they were sent; messages from several senders have no fixed order. in
whatever order the five processes happened to run, so the program sorts them before it prints them; a program
whose output depends on that order is a program that prints differently from run to run. The sixth
process.receive(results, within: 100) found no message and returned Error(Nil) after 100 ms: a receive with a
timeout is a Result, so waiting forever is never an accident.
Receiving on a subject another process made
Only the owner of a subject may receive from it; the documentation says receive panics when another process
tries. Make the subject in the process that will wait for the answers, as main does here, and pass it to the
processes that send.
Checkpoint
Check: Processes and subjects
An actor is a process that keeps state and handles one message at a time
A process that loops over its mailbox and keeps some state is the commonest thing on the BEAM, and gleam_otp’s
actor (gleam_otp): A process that keeps state and handles the messages of one type one at a time: a handler takes the current state and a message and returns the next state. gleam_otp's actor module provides it, with a typed interface. is that loop, typed. Its documentation compares it with Erlang’s gen_server and
Elixir’s GenServer, with a fully typed interface, and says an actor handles messages sequentially, one after the
other. You write two things: the type of the messages it accepts, and a handler that takes the current state and one
message and returns the next state with actor.continue. A message that wants an answer carries a subject for it,
and process.call creates that subject, sends the message and waits for the reply:
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/io
import gleam/list
import gleam/otp/actor
import gleam/result
/// The messages the stock actor understands. A message that wants an answer
/// carries the subject to send the answer to.
pub type Message {
Reserve(units: Int, reply: Subject(Result(Int, String)))
Release(units: Int)
Left(reply: Subject(Int))
}
/// The current stock and one message in, the next stock out.
pub fn handle(left: Int, message: Message) -> actor.Next(Int, Message) {
case message {
Reserve(units:, reply:) if units <= left -> {
process.send(reply, Ok(left - units))
actor.continue(left - units)
}
Reserve(reply:, ..) -> {
process.send(reply, Error("only " <> int.to_string(left) <> " left"))
actor.continue(left)
}
Release(units:) -> actor.continue(left + units)
Left(reply:) -> {
process.send(reply, left)
actor.continue(left)
}
}
}
pub fn start(units: Int) -> Subject(Message) {
let assert Ok(started) =
actor.new(units) |> actor.on_message(handle) |> actor.start
started.data
}
pub fn main() -> Nil {
let stock = start(5)
let _ = echo process.call(stock, 100, Reserve(2, _))
let _ = echo process.call(stock, 100, Reserve(4, _))
process.send(stock, Release(1))
echo process.call(stock, 100, Left)
// Ten buyers at once, each a process asking for one unit.
let answers = process.new_subject()
list.each(list.repeat(Nil, 10), fn(_) {
process.spawn(fn() {
process.send(answers, process.call(stock, 1000, Reserve(1, _)))
})
})
let results =
list.map(list.repeat(Nil, 10), fn(_) {
let assert Ok(answer) = process.receive(answers, within: 1000)
answer
})
let sold = list.count(results, result.is_ok)
io.println(int.to_string(sold) <> " of 10 buyers got a unit")
echo list.unique(list.filter(results, result.is_error))
echo process.call(stock, 100, Left)
Nil
} main starts the actor with 5 units, makes three calls and one send, then starts ten processes that each ask for
one unit at the same time. The session runs it, then the module’s three tests:
from session import run
# The stock actor: three calls and a send, then ten buyers at once against the four units left, then its tests.
run(
"""
cd processes
gleam run -m stock
gleam test
"""
) Output
$ cd processes
$ gleam run -m stock
Downloading packages
Downloaded 4 packages in …s
Compiling gleam_stdlib
Compiling gleam_erlang
Compiling gleam_otp
Compiling gleeunit
Compiling processes
Compiled in …s
Running stock.main
src/stock.gleam:43
Ok(3)
src/stock.gleam:44
Error("only 3 left")
src/stock.gleam:46
4
4 of 10 buyers got a unit
src/stock.gleam:62
[Error("only 0 left")]
src/stock.gleam:63
0
$ gleam test
Compiled in …s
Running processes_test.main
...
3 passed, no failures
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 actor_session.py
The first calls ran in order: Reserve(2) replied Ok(3), Reserve(4) was refused with Error("only 3 left"), and
after Release(1) there were 4. Then exactly 4 of the 10 buyers got a unit, all six others got
Error("only 0 left"), and 0 units were left. Step through what happened when two of the buyers reached the last
unit:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Three units sold. Three buyers have already been served, one unit each, so the actor's state is 1: the last of the 4 units left after Reserve(2), the refused Reserve(4) and Release(1).
- Two calls at once. Buyers A and B both call process.call(stock, 1000, Reserve(1, _)). Each call sends a message that carries a new subject for the reply, then waits.
- A queue, not a race. Both messages wait in the actor's mailbox. The actor runs handle for one message at a time, so the second waits until the first is done.
- A gets the unit. handle(1, Reserve(1, …)): 1 <= 1, so it replies Ok(0) and continues with state 0. Nothing else ran while it did.
- B is told no. handle(0, Reserve(1, …)): the guard fails, so it replies Error("only 0 left") and keeps state 0. No unit is sold twice.
- The whole run. Across all ten buyers this is the session's result: 4 of 10 buyers got a unit, the other six got the same Error, and 0 units are left.
No code took a lock: the ten messages waited in one mailbox, and handle ran for one at a time, each time against
the state the previous message left. The tests make it a rule, with twenty buyers against five units.
Key idea
An actor owns its state and handles one message at a time, so a check and an update in the same handle call can
never be split by another request: the actor serialises them.
Checkpoint
Check: Actors that keep state
A supervisor restarts a crashed actor, but not its state
The BEAM’s answer to a crash is to let the process die and start a fresh one. A supervisor: A process that starts other processes, its children, and starts a child again when it ends with a crash, by calling its start function, so the new child begins from its starting state.
does that: gleam_otp’s static supervisor starts its children and, under the OneForOne strategy, restarts only
the child that ended. The child here is the stock actor, registered under a name made with process.new_name, so
that callers keep a named subject that reaches whichever process holds the name. The handler has one extra message,
Crash, which panics:
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/io
import gleam/otp/actor
import gleam/otp/static_supervisor as supervisor
import gleam/otp/supervision
pub type Message {
Reserve(units: Int, reply: Subject(Result(Int, String)))
Left(reply: Subject(Int))
Crash
}
fn handle(left: Int, message: Message) -> actor.Next(Int, Message) {
case message {
Reserve(units:, reply:) if units <= left -> {
process.send(reply, Ok(left - units))
actor.continue(left - units)
}
Reserve(reply:, ..) -> {
process.send(reply, Error("only " <> int.to_string(left) <> " left"))
actor.continue(left)
}
Left(reply:) -> {
process.send(reply, left)
actor.continue(left)
}
Crash -> panic as "the stock actor was told to crash"
}
}
/// The child the supervisor starts, and starts again after a crash: an actor
/// with 5 units, registered under `name` so that callers never hold its pid.
fn start(name: process.Name(Message)) {
actor.new(5)
|> actor.on_message(handle)
|> actor.named(name)
|> actor.start
}
pub fn main() -> Nil {
let name = process.new_name("stock")
let assert Ok(_) =
supervisor.new(supervisor.OneForOne)
|> supervisor.add(supervision.worker(fn() { start(name) }))
|> supervisor.start
let stock = process.named_subject(name)
let _ = echo process.call(stock, 100, Reserve(2, _))
process.send(stock, Crash)
process.sleep(100)
io.println("after the restart:")
let _ = echo process.call(stock, 100, Left)
Nil
} give_up.gleam is the same program with a different main: it sends Crash every 100 ms, forever. Step through
both runs, then read what the BEAM printed:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Start under a supervisor. main starts a supervisor with one worker: an actor with 5 units, registered under a name. main keeps a named subject, not a pid.
- Reserve 2. A call through the named subject reaches the actor, which replies Ok(3) and continues with 3 units.
- The crash. main sends Crash, and handle panics. The process ends, and the BEAM logs a crash report with the panic message.
- The supervisor notices. The supervisor is linked to its child, so it learns of the exit at once and logs a supervisor report: errorContext child_terminated.
- Started again. The child is permanent, so the supervisor calls start(name) again: a new process, with 5 units, under the same name.
- The name still works. main's named subject now reaches the new process, and Left answers 5: the reservation of 2 units is gone with the old process.
- Too many crashes. give_up crashes the actor every 100 ms. The supervisor restarts it twice; the third crash within 5 seconds passes its default limit of 2 restarts.
- The supervisor stops. The supervisor shuts down with its children, and the exit signal reaches main, which is linked to it: the program ended with exit reason Shutdown.
from session import run
# A named stock actor under a one-for-one supervisor. supervised crashes it once, in full; give_up crashes it every
# 100 ms until the supervisor stops, and its output goes to give_up.log, of which the session shows the lines that say
# what happened (each crash, each report's errorContext, the supervisor's reason and how the program ended).
run(
"""
cd processes
gleam run -m supervised
!gleam run -m give_up > give_up.log 2>&1
grep -E '^crash|errorContext|reached_max|exit reason|^ Shutdown' give_up.log
"""
) Output
$ cd processes
$ gleam run -m supervised
Downloading packages
Downloaded 4 packages in …s
Compiling gleam_stdlib
Compiling gleam_erlang
Compiling gleam_otp
Compiling gleeunit
Compiling processes
Compiled in …s
Running supervised.main
src/supervised.gleam:49
Ok(3)
=CRASH REPORT==== … ===
crasher:
initial call: gleam@otp@actor:'-start/1-anonymous-0-'/0
pid: <0.91.0>
registered_name: 'stock$…'
exception error: #{function => <<"handle">>,line => 28,
message => <<"the stock actor was told to crash">>,
module => <<"supervised">>,
file => <<"src/supervised.gleam">>,
gleam_error => panic}
in function supervised:handle/2 (src/supervised.gleam:31)
in call from gleam@otp@actor:loop/1 (src/gleam/otp/actor.gleam:491)
ancestors: [<0.90.0>,<0.89.0>]
message_queue_len: 0
messages: []
links: [<0.90.0>]
dictionary: []
trap_exit: false
status: running
heap_size: 376
stack_size: 29
reductions: …
neighbours:
=SUPERVISOR REPORT==== … ===
supervisor: {<0.90.0>,gleam@otp@static_supervisor}
errorContext: child_terminated
reason: {#{function => <<"handle">>,line => 28,
message => <<"the stock actor was told to crash">>,
module => <<"supervised">>,file => <<"src/supervised.gleam">>,
gleam_error => panic},
[{supervised,handle,2,[{file,"src/supervised.gleam"},{line,31}]},
{gleam@otp@actor,loop,1,
[{file,"src/gleam/otp/actor.gleam"},
{line,491}]},
{proc_lib,init_p,3,[{file,"proc_lib.erl"},{line,317}]}]}
offender: [{pid,<0.91.0>},
{id,0},
{mfargs,
{gleam@otp@static_supervisor,start_child_callback,
[#Fun<gleam@otp@supervision.0.84844907>]}},
{restart_type,permanent},
{significant,false},
{shutdown,5000},
{child_type,worker}]
after the restart:
src/supervised.gleam:53
5
$ gleam run -m give_up > give_up.log 2>&1
[exit status 1]
$ grep -E '^crash|errorContext|reached_max|exit reason|^ Shutdown' give_up.log
crash 1
errorContext: child_terminated
crash 2
errorContext: child_terminated
crash 3
errorContext: child_terminated
errorContext: shutdown
reason: reached_max_restart_intensity
exit reason:
Shutdown
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 supervisor_session.py
The crash printed two reports. The crash report names the process, its registered name and the panic’s message, file
and line; the supervisor report says errorContext: child_terminated and lists the child’s settings, among them
restart_type,permanent, the default of supervision.worker. After the restart the named subject reached the new
process, and Left answered 5, not 3: the reservation lived in the old process’s state and died with it.
give_up shows the limit. The supervisor restarted the actor after the first and the second crash, and after the
third it stopped with reached_max_restart_intensity: the documentation of static_supervisor sets the default
restart intensity: The limit on how often a supervisor restarts its children: more than that many restarts within a period makes it shut down its children and itself. gleam_otp's static supervisor allows 2 restarts in 5 seconds by default. to 2 restarts in 5 seconds, and more than that makes the
supervisor shut down its children and itself. main was linked to it, so the program ended too, with the exit reason
Shutdown. A supervisor stops a crash loop instead of hiding it.
State that must survive a crash
A restart begins from the actor’s starting state, so anything the business cannot lose, such as a confirmed booking or a payment, must be written somewhere that outlives the process before the actor replies: a database, or a log the new actor reads back when it starts.
Checkpoint
Check: Supervisors and restarts
@external calls Erlang or JavaScript, and the compiler trusts its type
Gleam reaches code written in other languages through external function: A Gleam function with no Gleam body and an @external attribute for each target, naming the Erlang module or JavaScript file and the function that implement it. The compiler trusts the type written on it.: a
function with no Gleam body and one @external attribute for each target, naming the module and function that
implement it. The language tour adds the rule that matters most: the compiler has no way to read the types of code
in another language, so it takes the type you declare on trust, and a wrong one can crash at run time.
ffi.gleam binds runtime() to a small Erlang module and a JavaScript file in its own src folder:
ffi/src/ffi.gleam
import gleam/int
import gleam/io
import gleam/string
/// The runtime this code runs on. The compiler cannot read Erlang or
/// JavaScript, so it trusts the type written here: String.
@external(erlang, "ffi_ffi", "runtime")
@external(javascript, "./ffi_ffi.mjs", "runtime")
pub fn runtime() -> String
pub fn main() -> Nil {
let name = runtime()
io.println("running on " <> name)
io.println(
"the name has " <> int.to_string(string.length(name)) <> " characters",
)
} ffi/src/ffi_ffi.erl
-module(ffi_ffi).
-export([runtime/0]).
%% Returns a binary, which is what a Gleam String is on the BEAM.
runtime() ->
Release = list_to_binary(erlang:system_info(otp_release)),
<<"Erlang/OTP ", Release/binary>>. ffi/src/ffi_ffi.mjs
// Returns a JavaScript string, which is what a Gleam String is on JavaScript.
export function runtime() {
return `Node.js ${process.versions.node}`;
} wrong_type.gleam gets it wrong on purpose: erlang:length/1 counts the items of a list, and the binding says it
counts a String:
import gleam/int
import gleam/io
/// erlang:length/1 counts the items of a list. The type written here says it
/// counts a String, and the compiler cannot tell that this is wrong.
@external(erlang, "erlang", "length")
fn count(text: String) -> Int
pub fn main() -> Nil {
io.println("calling erlang:length on a Gleam String")
io.println(int.to_string(count("Gleam")))
} Step through both projects, then run them:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A function without a body. runtime() has a type, -> String, and no Gleam body. Its two @external lines name the Erlang module and function, and the JavaScript file and export, that implement it.
- The type is trusted. The type checker cannot read Erlang or JavaScript, so it takes -> String on trust and checks every Gleam caller against it.
- On the BEAM. The Erlang build calls ffi_ffi:runtime/0, which builds a binary, the BEAM's form of a Gleam String: running on Erlang/OTP 29, 13 characters.
- On Node.js. The JavaScript build imports runtime from ffi_ffi.mjs, which returns a JavaScript string: running on Node.js 24.21.0, 15 characters.
- A wrong type compiles. wrong_type.gleam binds erlang:length/1, which counts the items of a list, as fn(String) -> Int. The Gleam compiler accepts it, because it trusts the type.
- It fails at run time. Called with "Gleam", a binary, erlang:length raised badarg and the program stopped with exit status 1. Erlang's own compiler warned, but only because the argument was a literal.
from session import run
# One Gleam function with an external for each target, run on both; how the standard library binds int.to_string
# the same way; then a binding whose type is wrong, which the Gleam compiler accepts.
run(
"""
cd ffi
gleam run
gleam run --target javascript
grep -n -B 2 '^pub fn to_string' build/packages/gleam_stdlib/src/gleam/int.gleam
cd ../wrong_type
!gleam run
"""
) Output
$ cd ffi
$ gleam run
Downloading packages
Downloaded 2 packages in …s
Compiling gleam_stdlib
Compiling gleeunit
Compiling ffi
Compiled in …s
Running ffi.main
running on Erlang/OTP 29
the name has 13 characters
$ gleam run --target javascript
Compiling gleam_stdlib
Compiling gleeunit
Compiling ffi
Compiled in …s
Running ffi.main
running on Node.js 24.21.0
the name has 15 characters
$ grep -n -B 2 '^pub fn to_string' build/packages/gleam_stdlib/src/gleam/int.gleam
147-@external(erlang, "erlang", "integer_to_binary")
148-@external(javascript, "../gleam_stdlib.mjs", "to_string")
149:pub fn to_string(x: Int) -> String
$ cd ../wrong_type
$ gleam run
Downloading packages
Downloaded 2 packages in …s
Compiling gleam_stdlib
Compiling gleeunit
Compiling wrong_type
src/wrong_type.gleam:13:51: Warning: the call to length/1 will fail with a 'badarg' exception
Compiled in …s
Running wrong_type.main
calling erlang:length on a Gleam String
runtime error: Erlang error
An error occurred outside of Gleam.
erlang:error(Badarg)
stacktrace:
erlang.length unknown source
wrong_type.main src/wrong_type.gleam:13
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ffi_session.py
On the BEAM runtime() returned Erlang/OTP 29, a binary built by the Erlang function, and on Node.js
Node.js 24.21.0, a JavaScript string from process.versions.node; string.length worked on both because each
implementation returned what a Gleam String is on its target. The standard library binds its own functions the same
way: int.to_string has no Gleam body, only erlang:integer_to_binary and a function of gleam_stdlib.mjs.
The wrong binding compiled without a word from Gleam. Erlang’s own compiler warned that the call would fail with
badarg, only because the argument was a literal it could see, and at run time the program stopped with
erlang:error(Badarg) and exit status 1. With a value from anywhere else, nothing would have warned.
Binding a foreign function by reading its name only
Read the foreign function’s documentation for every argument and the return value, as Erlang’s documentation of
length/1 says it takes a list. When the types do not line up, write a small .erl or .mjs wrapper that
converts them, as ffi_ffi.erl turns a charlist into a binary, and test the binding on each target it claims.
Checkpoint
Check: External functions on each target
Practise: the handler of a rate-limiter actor
An actor is a loop around one function, the state and a message in, the next state and a reply out, so its logic can be written and tested without any process at all. The exercise writes that function for a rate limiter, three requests per user in each 1,000 ms window, in Python so that its tests run in your browser; the tests send it scripted message sequences the way a mailbox would, including the edge at 1,000 ms and a state that must not change in place:
Exercise · Medium · Python
The message handler of a rate-limiter actor
An actor is a loop around one function: the state and a message in, the next state and a reply out. Write that function for a rate limiter, in Python so that its tests run in your browser: handle(state, message) in limiter.py returns (new_state, reply), where the state is a dict and must not be changed in place, so that the caller can keep the old one, as an actor's actor.continue(next) does.
("request", user, at), withatin whole milliseconds, asks whetherusermay make a request now. Time is cut into fixed windows ofWINDOW= 1000 ms, numberedat // WINDOW: 0 to 999 is window 0, 1000 to 1999 window 1. A user may makeLIMIT= 3 requests in one window. The reply isTruefor an allowed request, which counts, andFalsefor a refused one, which does not.("forget", user)removes the user from the state, as a reset by a support desk would; the reply isNone.- Users are counted apart, and the state starts as
{}. Times of one user never go backwards.
Keep for each user only their window number and their count in it, so that the state does not grow with the number of requests.
Starter code · limiter.py
LIMIT = 3
WINDOW = 1000
def handle(state, message):
"""One message to the rate limiter: returns (new_state, reply)."""
# Replace this with your code: it allows every request and never counts.
if message[0] == "request":
return (state, True)
return (state, None) The sample tests · test_limiter.py
from limiter import handle
def run(messages, state=None):
"""Sends the messages one after another, as an actor's mailbox would, and returns the replies."""
state = {} if state is None else state
replies = []
for message in messages:
state, reply = handle(state, message)
replies.append(reply)
return replies
def test_three_requests_then_a_refusal():
"""the fourth request of a window is refused"""
assert run([("request", "asha", t) for t in (10, 20, 30, 40)]) == [True, True, True, False]
def test_the_next_window_starts_again():
"""1000 ms is the first instant of window 1"""
assert run([("request", "asha", t) for t in (0, 1, 2, 999, 1000)]) == [True, True, True, False, True]
def test_users_are_counted_apart():
"""ravi's requests do not use asha's"""
replies = run([("request", "asha", 5)] * 3 + [("request", "ravi", 6), ("request", "asha", 7)])
assert replies == [True, True, True, True, False]
def test_refused_requests_do_not_count():
"""after a refusal in window 0, window 1 still allows three"""
messages = [("request", "meera", t) for t in (100, 200, 300, 400, 1100, 1200, 1300, 1400)]
assert run(messages) == [True, True, True, False, True, True, True, False]
def test_forget_resets_a_user():
"""a forgotten user starts again in the same window"""
messages = [("request", "joe", t) for t in (1, 2, 3)] + [("forget", "joe"), ("request", "joe", 4)]
assert run(messages) == [True, True, True, None, True]
def test_the_old_state_is_not_changed():
"""an actor keeps the old state until handle returns the new one"""
before = {"asha": (0, 2)}
after, reply = handle(before, ("request", "asha", 500))
assert (reply, before, after) == (True, {"asha": (0, 2)}, {"asha": (0, 3)}) A hint
Look up the user's (window, count). If the user is new, or their window is not the current one, the count starts again at 0. Then compare the count with LIMIT, and build the new state with {**state, user: (window, count + 1)} only when the request is allowed.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the self-check: it begins with whichever of the four ideas you are shakiest on, from subjects to externals, and every option you choose comes with its reason:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to mid-level): What is a subject in Gleam, and why does Gleam send messages through one instead of to a process id?
A subject is a value through which processes send each other messages of one type: Subject(Int) carries Ints, and
the compiler refuses a message of any other type. It belongs to the process that created it: any process may send
to it, and only that owner may receive from it, with process.receive(subject, within: ms), which returns
Error(Nil) when the timeout passes. A plain Erlang process id accepts any message at all, so a mistake shows up as
a message nobody handles; a subject puts the message type in the type system. In the lesson, main made one subject,
five processes sent squares to it and main summed them to 55. A named subject goes further: it reaches whichever
process holds a name, which is how callers kept talking to the stock actor after its restart.
Two longer questions, one about lost state and one about a binding:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to staff
A Gleam service keeps each shopping cart in an actor under a supervisor. After a bad deploy, users report empty carts that nobody cleared, and the logs show crash reports for cart actors. Explain what happened, and redesign it so that a crash never loses what a customer added.
-
Mid to senior
A Gleam service binds an Erlang library function with @external, typed fn(String) -> Int. Its tests pass, but in production some requests crash with erlang:error(Badarg). The Gleam compiler never warned. Why, how do you find the bad input, and how do you fix the binding for good?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is a process on the BEAM?
A lightweight unit of execution with its own memory and its own mailbox. Processes share no data and cooperate by sending messages; in Gleam, process.spawn starts one.
-
Who may send to a Gleam subject, and who may receive from it?
Any process may send to it, but only the process that created it may receive; receive panics anywhere else. Create the subject in the process that waits for the answers.
-
What does process.receive(results, within: 100) give when no message arrives?
Error(Nil), after 100 ms. A receive with a timeout is a Result, so a process never waits forever by accident.
-
Why did the program sort the five squares before printing them?
Messages reach the mailbox in whatever order the processes happened to run, so output that depended on that order would change from run to run.
-
Why could two buyers never both take the last unit from the stock actor?
The actor takes its messages one by one, each against the state the previous one left, so a check and an update in one handler call are never split. 4 of 10 buyers got the 4 units, with no lock.
-
A supervised stock actor crashes after reserving 2 of 5 units. What does Left answer after the restart?
5. A supervisor restarts a child from its starting state, so the reservation died with the old process. State that must survive goes to a database or a log first.
-
When does a gleam_otp static supervisor stop restarting a crashing child?
After more than 2 restarts within 5 seconds, its default restart intensity. It then ends with reached_max_restart_intensity, shuts down its children and takes linked callers down too.
-
What does the Gleam compiler check about the type of an @external function?
Nothing: it cannot read the foreign code, so it trusts the declared type. Binding erlang:length/1 to a String compiled and then stopped at run time with badarg.
Key takeaways
process.spawnstarts a process beside the current one, a subject carries typed messages to the process that owns it, andprocess.receivewith a timeout returnsError(Nil)instead of waiting forever.- A
gleam_otpactor is a typed message loop: a handler takes the state and one message and returns the next state, so a check and an update in one message cannot be interleaved, and ten buyers got only the four units there were. - A supervisor restarts a crashed child from its starting state, by the same name, and gives up after more than 2 restarts in 5 seconds by default, taking its linked callers down with it.
- An
@externalfunction names an implementation per target, and the compiler trusts its type: a wrong type compiles and fails at run time, so wrap foreign functions whose types do not match and test each target.
Terms in this lesson
- actor (gleam_otp)
- A process that keeps state and handles the messages of one type one at a time: a handler takes the current state and a message and returns the next state. gleam_otp's actor module provides it, with a typed interface.
- external function
- A Gleam function with no Gleam body and an @external attribute for each target, naming the Erlang module or JavaScript file and the function that implement it. The compiler trusts the type written on it.
- mailbox
- The queue of messages that a BEAM process has received and not yet handled. Messages from one sender arrive in the order they were sent; messages from several senders have no fixed order.
- process (BEAM)
- A lightweight unit of execution on the BEAM, with its own memory and its own mailbox. Processes share nothing and work together by sending each other messages; in Gleam, process.spawn from gleam_erlang starts one.
- restart intensity
- The limit on how often a supervisor restarts its children: more than that many restarts within a period makes it shut down its children and itself. gleam_otp's static supervisor allows 2 restarts in 5 seconds by default.
- subject (Gleam)
- A value through which processes send messages of one type, such as Subject(Int). It is owned by the process that created it: any process may send to it, and only the owner may receive from it.
- supervisor
- A process that starts other processes, its children, and starts a child again when it ends with a crash, by calling its start function, so the new child begins from its starting state.
References
- gleam_erlang: gleam/erlang/process (Gleam)
- gleam_otp: gleam/otp/actor (Gleam)
- gleam_otp: gleam/otp/static_supervisor (Gleam)
- gleam_otp: gleam/otp/supervision (Gleam)
- The Gleam language tour: externals and multi-target externals (Gleam)
- Erlang reference manual: processes (Erlang/OTP team)
- Erlang: concurrent programming (processes share no data) (Erlang/OTP team)
- Erlang/OTP: supervisor behaviour (restart intensity) (Erlang/OTP team)
- Erlang/OTP: erlang:length/1 (Erlang/OTP team)
- Node.js: process.versions (OpenJS Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress