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.

Leave a Comment

Your email address will not be published. Required fields are marked *