Skip to content

Add Gleam + Mist: first Gleam and BEAM VM entry! 🌟#24

Merged
MDA2AV merged 4 commits intoMDA2AV:mainfrom
BennyFranciscus:add-gleam-mist
Mar 16, 2026
Merged

Add Gleam + Mist: first Gleam and BEAM VM entry! 🌟#24
MDA2AV merged 4 commits intoMDA2AV:mainfrom
BennyFranciscus:add-gleam-mist

Conversation

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

What's this?

Adds Mist — a Gleam HTTP server running on the BEAM (Erlang VM).

This is the first Gleam framework AND the first BEAM VM entry in HttpArena!

Why it's interesting

The BEAM VM is a completely different runtime model than anything currently in HttpArena. While most entries use async/await (Tokio, libuv, etc.) or OS threads, the BEAM uses lightweight processes with preemptive scheduling and per-process garbage collection. Each connection gets its own isolated process — if one crashes, nothing else is affected.

Gleam itself is a really cool language — type-safe, functional, compiles to Erlang bytecode, and the pattern-matching-based routing is clean and fast.

Stack

  • Language: Gleam (~479⭐ for Mist, ~1,388⭐ for the Wisp framework that sits on top)
  • Runtime: BEAM (Erlang VM / OTP)
  • HTTP server: Mist (OTP process per connection)
  • JSON: gleam_json
  • SQLite: sqlight (NIF bindings)
  • Compression: Erlang's built-in zlib

Endpoints implemented

  • /pipeline — plain text
  • /baseline11 — GET/POST with query param sum
  • /baseline2 — GET with query param sum
  • /json — JSON serialization (pre-cached)
  • /compression — gzip via Erlang zlib
  • /upload — POST body size
  • /db — SQLite query
  • /static/{filename} — static file serving

cc @rawhat — thought it'd be cool to see how Mist stacks up in HttpArena! The BEAM's concurrency model is so different from everything else in here, should make for really interesting benchmark comparisons.

Adds Mist HTTP server for Gleam running on the BEAM (Erlang VM).
This is the first Gleam framework and first BEAM VM entry in HttpArena.

- Language: Gleam (compiles to Erlang bytecode)
- Runtime: BEAM (Erlang VM / OTP)
- HTTP: Mist (OTP process per connection)
- JSON: gleam_json
- SQLite: sqlight (NIF bindings)
- Compression: Erlang's built-in zlib

The BEAM's preemptive scheduling and per-process GC represents a
fundamentally different concurrency model than async/await or OS threads.
gleam_stdlib requires Gleam >= 1.14.0, was using v1.8.0.
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Build fix: bumped Gleam from v1.8.0 to v1.14.0 in the Dockerfile. The latest gleam_stdlib requires >= 1.14.0.

decode.at returns Decoder(a) directly (2 args) and can't be used with
`use` callbacks. decode.subfield takes a path + decoder + callback (3 args),
which is what we need for the `use` pattern in the dataset decoder.

This fixes the Gleam 1.14 build error.
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Build fix: decode.at in Gleam 1.14+ returns Decoder(a) directly (2 args) — it doesn't accept a callback, so it can't be used with the use pattern. Switched to decode.subfield which takes path + decoder + callback (3 args). Should compile clean now.

Gleam 1.14's build image ships OTP 28, so BEAM files compiled there
won't load on the OTP 27 runtime. Bumped runtime to erlang:28-alpine.

Also added mist.bind("0.0.0.0") so the server listens on all
interfaces — required for Docker networking (was binding 127.0.0.1).
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Found the startup issue — two problems:

  1. OTP version mismatch: Gleam 1.14's build image ships OTP 28, but the runtime stage was using erlang:27-alpine. BEAM files compiled on OTP 28 won't load on 27 (the @@ module naming hits an undef error). Bumped to erlang:28-alpine.

  2. Bind address: Mist defaults to 127.0.0.1, which doesn't work in Docker — the health check can't reach it from outside the container. Added mist.bind("0.0.0.0").

Should start up cleanly now. 🤞

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: gleam-mist | Profile: all profiles

gleam-mist / baseline / 512c (p=1, r=0, cpu=unlimited)
  Best: 365747 req/s (CPU: 7898.4%, Mem: 1.4GiB) ===

gleam-mist / baseline / 4096c (p=1, r=0, cpu=unlimited)
  Best: 741053 req/s (CPU: 8650.0%, Mem: 2.1GiB) ===

gleam-mist / baseline / 16384c (p=1, r=0, cpu=unlimited)
  Best: 662016 req/s (CPU: 7932.5%, Mem: 3.6GiB) ===

gleam-mist / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 356056 req/s (CPU: 7426.4%, Mem: 508.4MiB) ===

gleam-mist / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 351716 req/s (CPU: 6864.8%, Mem: 1.5GiB) ===

gleam-mist / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 847360 req/s (CPU: 7468.0%, Mem: 2.4GiB) ===

gleam-mist / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 109966 req/s (CPU: 1287.9%, Mem: 428.8MiB) ===

gleam-mist / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 110240 req/s (CPU: 1342.4%, Mem: 455.4MiB) ===

gleam-mist / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 307730 req/s (CPU: 6637.0%, Mem: 1.4GiB) ===

gleam-mist / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 266696 req/s (CPU: 6361.0%, Mem: 911.9MiB) ===

gleam-mist / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 322 req/s (CPU: 1027.1%, Mem: 10.0GiB) ===

gleam-mist / upload / 256c (p=1, r=0, cpu=unlimited)
  Best: 314 req/s (CPU: 1250.9%, Mem: 19.3GiB) ===

gleam-mist / upload / 512c (p=1, r=0, cpu=unlimited)
  Best: 287 req/s (CPU: 1240.7%, Mem: 21.5GiB) ===

gleam-mist / compression / 4096c (p=1, r=0, cpu=unlimited)
  Best: 3931 req/s (CPU: 12219.6%, Mem: 865.4MiB) ===

gleam-mist / compression / 16384c (p=1, r=0, cpu=unlimited)
  Best: 3834 req/s (CPU: 11671.3%, Mem: 805.5MiB) ===

gleam-mist / noisy / 512c (p=1, r=0, cpu=unlimited)
  Best: 271894 req/s (CPU: 7798.9%, Mem: 1012.0MiB) ===

gleam-mist / noisy / 4096c (p=1, r=0, cpu=unlimited)
  Best: 463383 req/s (CPU: 7945.6%, Mem: 2.4GiB) ===

gleam-mist / noisy / 16384c (p=1, r=0, cpu=unlimited)
  Best: 529205 req/s (CPU: 7683.9%, Mem: 4.3GiB) ===

gleam-mist / mixed / 4096c (p=1, r=5, cpu=unlimited)
  Best: 16254 req/s (CPU: 11559.4%, Mem: 1.6GiB) ===

gleam-mist / mixed / 16384c (p=1, r=5, cpu=unlimited)
  Best: 14037 req/s (CPU: 9889.8%, Mem: 1.8GiB) ===
Full log
  Per-template-ok: 1613109,1032916,0,0,0

  WARNING: 1028340/3674365 responses (28.0%) had unexpected status (expected 2xx)
  CPU: 7683.9% | Mem: 4.3GiB

=== Best: 529205 req/s (CPU: 7683.9%, Mem: 4.3GiB) ===
  Input BW: 53.50MB/s (avg template: 106 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-gleam-mist
httparena-bench-gleam-mist

==============================================
=== gleam-mist / mixed / 4096c (p=1, r=5, cpu=unlimited) ===
==============================================
3e6be902cd723278d04e4e0329e3b08568a5a83543a51f5de67db49e8b852f64
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  5
  Templates: 10
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   86.06ms   1.23ms   324.80ms   531.50ms   687.90ms

  89487 requests in 5.00s, 79927 responses
  Throughput: 15.97K req/s
  Bandwidth:  509.67MB/s
  Status codes: 2xx=79927, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 79927 / 79927 responses (100.0%)
  Reconnects: 17548
  Errors: connect 0, read 134, timeout 0
  Per-template: 8030,8226,8320,8704,8977,7516,7782,8534,7129,6709
  Per-template-ok: 8030,8226,8320,8704,8977,7516,7782,8534,7129,6709
  CPU: 10999.6% | Mem: 2.1GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  5
  Templates: 10
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   68.66ms   1.09ms   293.80ms   366.00ms   524.00ms

  90568 requests in 5.00s, 81270 responses
  Throughput: 16.25K req/s
  Bandwidth:  516.00MB/s
  Status codes: 2xx=81270, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 81269 / 81270 responses (100.0%)
  Reconnects: 17735
  Errors: connect 0, read 104, timeout 0
  Per-template: 8276,8495,8663,8878,9078,7410,7526,8902,7124,6917
  Per-template-ok: 8276,8495,8663,8878,9078,7410,7526,8902,7124,6917
  CPU: 11559.4% | Mem: 1.6GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  5
  Templates: 10
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   70.81ms   1.08ms   304.00ms   364.00ms   533.30ms

  88238 requests in 5.00s, 79710 responses
  Throughput: 15.93K req/s
  Bandwidth:  510.09MB/s
  Status codes: 2xx=79710, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 79710 / 79710 responses (100.0%)
  Reconnects: 17361
  Errors: connect 0, read 28, timeout 0
  Per-template: 8450,8434,8556,8557,8686,7104,7228,8793,7072,6830
  Per-template-ok: 8450,8434,8556,8557,8686,7104,7228,8793,7072,6830
  CPU: 11235.1% | Mem: 1.6GiB

=== Best: 16254 req/s (CPU: 11559.4%, Mem: 1.6GiB) ===
  Input BW: 1.59GB/s (avg template: 104924 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-gleam-mist
httparena-bench-gleam-mist

==============================================
=== gleam-mist / mixed / 16384c (p=1, r=5, cpu=unlimited) ===
==============================================
7334bfd0d6a65cef937b2f62bba81b599eebdad77a90bbc193fcd297cc0ffad0
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  5
  Templates: 10
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   83.59ms   2.76ms   356.90ms   474.30ms   642.50ms

  79519 requests in 5.00s, 66536 responses
  Throughput: 13.29K req/s
  Bandwidth:  450.44MB/s
  Status codes: 2xx=66536, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 66536 / 66536 responses (100.0%)
  Reconnects: 15059
  Errors: connect 0, read 256, timeout 0
  Per-template: 7130,7201,7232,7231,7276,5754,5924,6455,6350,5983
  Per-template-ok: 7130,7201,7232,7231,7276,5754,5924,6455,6350,5983
  CPU: 9193.0% | Mem: 1.8GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  5
  Templates: 10
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   79.60ms   2.78ms   343.30ms   527.40ms   615.70ms

  81177 requests in 5.00s, 68954 responses
  Throughput: 13.78K req/s
  Bandwidth:  463.56MB/s
  Status codes: 2xx=68954, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 68954 / 68954 responses (100.0%)
  Reconnects: 15419
  Errors: connect 0, read 298, timeout 0
  Per-template: 7649,7645,7439,7368,7428,5907,5900,6918,6537,6163
  Per-template-ok: 7649,7645,7439,7368,7428,5907,5900,6918,6537,6163
  CPU: 10794.8% | Mem: 1.7GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  5
  Templates: 10
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   79.98ms   2.89ms   342.40ms   553.40ms   628.60ms

  81944 requests in 5.01s, 70326 responses
  Throughput: 14.05K req/s
  Bandwidth:  468.05MB/s
  Status codes: 2xx=70326, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 70326 / 70326 responses (100.0%)
  Reconnects: 15634
  Errors: connect 0, read 256, timeout 0
  Per-template: 7804,7589,7630,7697,7654,6001,5997,7127,6562,6265
  Per-template-ok: 7804,7589,7630,7697,7654,6001,5997,7127,6562,6265
  CPU: 9889.8% | Mem: 1.8GiB

=== Best: 14037 req/s (CPU: 9889.8%, Mem: 1.8GiB) ===
  Input BW: 1.37GB/s (avg template: 104924 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-gleam-mist
httparena-bench-gleam-mist
[skip] gleam-mist does not subscribe to baseline-h2
[skip] gleam-mist does not subscribe to static-h2
[skip] gleam-mist does not subscribe to baseline-h3
[skip] gleam-mist does not subscribe to static-h3
[skip] gleam-mist does not subscribe to unary-grpc
[skip] gleam-mist does not subscribe to unary-grpc-tls
[skip] gleam-mist does not subscribe to echo-ws
[restore] Restoring CPU governor to powersave...

@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Benchmarks are in! Here's the breakdown for Gleam + Mist on the BEAM VM:

🚀 Baseline: 741K req/s at 4096c — really solid for an Erlang VM framework. The BEAM's scheduler is doing great work distributing across cores.

📡 Pipelined: 847K req/s at 16384c — interesting that it peaks at higher concurrency rather than 512c. The BEAM handles massive connection counts gracefully, which is exactly what you'd expect from Erlang's heritage.

📊 JSON: 308K req/s at 4096c — reasonable, serialization is always going to be a heavier workload.

⏱️ Limited-conn: 110K req/s — consistent across connection counts, showing stable per-connection throughput.

📁 Upload: 322 req/s at 64c — in line with other frameworks on this test.

🗜️ Compression: 3.9K req/s — this is CPU-bound work so the BEAM's interpreted nature shows here.

🔀 Noisy: 529K req/s at 16384c — handles mixed traffic well. One note: the 16384c run showed 28% unexpected status codes which might be worth investigating. Could be the server getting overwhelmed at that concurrency level.

🎭 Mixed: 16.3K req/s at 4096c — the reconnect-heavy profile, decent showing.

Overall: really impressive for a BEAM language! Gleam brings type safety and a modern DX to the Erlang ecosystem, and Mist is putting up competitive numbers. The baseline throughput especially — 741K on the BEAM is no joke 🌟

@MDA2AV MDA2AV self-requested a review March 16, 2026 15:00
@MDA2AV MDA2AV merged commit 9ec8595 into MDA2AV:main Mar 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants