Let's cut the bullshit: BEAM isn't fast for CPU-heavy work. It's about as fast as Python, which means slow as shit for number crunching. But that's not the point - BEAM is built for latency and fault tolerance, not raw speed.
But BEAM has two performance superpowers that make it worth the tradeoff:
- Concurrency is very cheap - you can spawn 200,000+ concurrent processes like it's nothing
- VM is optimized for latency - per-process heap with no global stop-the-world, pre-emptive scheduling
This design proved its worth at scale: WhatsApp handles 40+ billion messages daily and Discord stores billions of messages using the same BEAM foundation that powers Gleam.
Memory Layout That Actually Matters
Every BEAM process gets four blocks of memory:
- Stack: Return addresses, function arguments, local variables
- Heap: Larger structures like lists and tuples
- Message area (mailbox): Messages from other processes
- Process Control Block: Process metadata
Each process burns about 2KB minimum, which sounds expensive until you realize you can spawn hundreds of thousands without your server catching fire. This lightweight process model is nothing like OS threads - it's what lets BEAM handle insane concurrency.
Why Your Gleam App Is Probably Slow
Pattern matching overhead: Gleam's pattern matching is powerful but not free. Efficient compilation of pattern matching is a surprisingly challenging problem, and complex nested patterns can create expensive dispatch trees.
List operations: Gleam lists are linked lists, not arrays. list.length()
is O(n), not O(1). If you're calling list.length(my_list) > 1000
, you're already fucked. I spent 6 hours debugging why our API went to shit - some genius was calling list.length()
on 50k-item lists in a hot path. Don't be that person.
No tail call optimization awareness: Gleam supports tail call optimization, but the compiler won't warn you when you're not using it. Writing recursive functions without proper accumulators will eat your stack.
String operations: BEAM strings are UTF-8 binaries. Concatenating strings creates new binaries every time. If you're building strings in loops, use `iodata` instead.
Performance Improvements That Actually Shipped
The Gleam team isn't sitting around - they shipped real performance wins:
- v1.11.0 got 30% faster JavaScript compilation in June 2025
- v1.12.0 enabled function inlining with conservative configuration
- Binary operations got optimized - taking a sub-slice is now constant time on JavaScript to match Erlang behavior
These aren't marketing bullshit numbers - Richard Viney actually benchmarked real workloads to prove it.
Debug Performance Issues First, Optimize Second
The `echo` keyword is your best friend for quick performance debugging:
import gleam/io
pub fn slow_function(data) {
data
|> echo("Input data size")
|> expensive_operation()
|> echo("After expensive operation")
|> another_expensive_operation()
|> echo("Final result")
}
The compiler tracks `echo` usage and will warn you if you try to publish with debug statements still in your code. Use it liberally while profiling, then remove it when you're done. The language server also provides code actions to remove all echos from a module.
Pro tip: Add timestamps to your echo statements:
import gleam/erlang/system_time
pub fn timed_operation(data) {
let start = system_time.monotonic_time()
let result = data
|> expensive_operation()
|> echo("Operation completed")
let end = system_time.monotonic_time()
let duration_ms = (end - start) / 1_000_000
io.println("Operation took " <> int.to_string(duration_ms) <> "ms")
result
}
This gives you microsecond-precision timing without external tools.