Gleam Processes
Processes are the unit of concurrency in Gleam. Each process runs independently, has its own memory, and communicates by sending messages. Millions of processes can run simultaneously on the BEAM without threads, locks, or shared state.
What Is a Process?
Process Model Diagram
──────────────────────────────────────────────────
Process A Process B Process C
┌─────────┐ ┌─────────┐ ┌─────────┐
│ own mem │ │ own mem │ │ own mem │
│ own pid │ msg→ │ own pid │ msg→ │ own pid │
└─────────┘ └─────────┘ └─────────┘
No shared memory — all coordination through messages.
A process is like a tiny program running inside your program. It has its own isolated memory, cannot access another process's memory directly, and communicates only through messages. A crash in one process does not affect others.
Spawning a Process
Use process.start from the gleam/erlang/process module:
import gleam/erlang/process
pub fn main() {
let _pid = process.start(fn() {
io.println("Hello from a new process!")
}, linked: False)
io.println("Main process continues here")
process.sleep(100)
}
Spawn Flow
──────────────────────────────────────────────────
main process
│
├── process.start(fn)
│ │
│ └── new process spawned
│ │
│ └── runs fn() independently
│
└── main continues immediately (doesn't wait)
Process IDs (PIDs)
Every process has a unique identifier called a PID. Store it to send messages or monitor the process:
import gleam/erlang/process
let pid = process.self() // your own PID
Sending and Receiving Messages
Processes communicate through message passing. Use Subject — a typed channel — to send and receive safely:
import gleam/erlang/process
pub fn main() {
let subject = process.new_subject()
// Spawn a worker that sends a reply
let _pid = process.start(fn() {
process.send(subject, "Hello from worker!")
}, linked: False)
// Main process waits for a message
let reply = process.receive(subject, 1000)
// Ok("Hello from worker!") within 1000ms
// Error(Nil) if timeout
case reply {
Ok(msg) -> io.println(msg)
Error(Nil) -> io.println("Timed out")
}
}
Message Passing Diagram
──────────────────────────────────────────────────
Worker process Main process
│ │
│ subject ────────────┤
│ │
│──send("Hello!")─ ────▶│
│
receive() → Ok("Hello!")
Subject — Typed Channels
A Subject(MessageType) is a typed mailbox. Only messages of the declared type can be sent through it — mismatched types are caught at compile time:
import gleam/erlang/process.{type Subject}
let int_channel: Subject(Int) = process.new_subject()
let str_channel: Subject(String) = process.new_subject()
process.send(int_channel, 42) // OK
process.send(str_channel, "hello") // OK
process.send(int_channel, "hello") // COMPILE ERROR
Linked Processes
Link a process to the parent. When one linked process crashes, the other is also terminated:
let _pid = process.start(worker_fn, linked: True)
// If worker crashes, the spawning process crashes too
// If spawning process crashes, worker is killed
Linked vs Unlinked
──────────────────────────────────────────────────
linked: True → crash propagates both ways
linked: False → processes are independent
Monitoring Processes
Monitor a process to receive a message when it terminates, without linking (no crash propagation):
import gleam/erlang/process
let subject = process.new_subject()
let pid = process.start(fn() {
io.println("Worker running")
}, linked: False)
let monitor = process.monitor_process(pid)
// When pid terminates, a Down message arrives
Process State Pattern
A process maintains state by calling itself recursively with updated state:
import gleam/erlang/process.{type Subject}
type Msg { Increment; GetCount(Subject(Int)) }
fn counter_loop(count: Int, subject: Subject(Msg)) -> Nil {
case process.receive(subject, -1) {
Ok(Increment) ->
counter_loop(count + 1, subject)
Ok(GetCount(reply_to)) -> {
process.send(reply_to, count)
counter_loop(count, subject)
}
Error(Nil) -> Nil // timeout — exit
}
}
Stateful Process Loop
──────────────────────────────────────────────────
counter_loop(0)
→ receive Increment → counter_loop(1)
→ receive Increment → counter_loop(2)
→ receive GetCount → send(2), counter_loop(2)
→ receive Increment → counter_loop(3)
Practical Example — Background Worker
import gleam/erlang/process
import gleam/io
type Job { ProcessFile(String); Shutdown }
fn worker(subject: process.Subject(Job)) -> Nil {
case process.receive(subject, -1) {
Ok(ProcessFile(path)) -> {
io.println("Processing: " <> path)
worker(subject)
}
Ok(Shutdown) -> io.println("Worker done")
Error(Nil) -> Nil
}
}
pub fn main() {
let chan = process.new_subject()
let _pid = process.start(fn() { worker(chan) }, linked: False)
process.send(chan, ProcessFile("report.csv"))
process.send(chan, ProcessFile("data.json"))
process.send(chan, Shutdown)
process.sleep(200)
}
Key Points
Process Essentials
──────────────────────────────────────────────────
1. Processes are isolated — no shared memory
2. Communicate only through messages
3. Subject(T) is a typed message channel
4. process.start(fn, linked:) spawns a new process
5. process.send(subject, msg) sends a message
6. process.receive(subject, timeout) waits for a message
7. linked: True propagates crashes both ways
8. Millions of processes can run concurrently on BEAM
Processes are the foundation of Gleam's concurrency model. Because processes share no memory, you never need locks or mutexes. The only way to share data is to send it — and the type system ensures you send the right kind of data every time.
