A minimal Rust microkernel purpose-built for BEAM.
No Linux. No POSIX. Just your Erlang/Elixir/Gleam code on bare metal.
Tyn is a unikernel — a single-purpose operating system kernel that hosts one thing: the BEAM virtual machine. It replaces the entire Linux stack with ~7,000 lines of Rust, targeting KVM/QEMU cloud deployments.
The BEAM already has its own process model, scheduler, memory management, and distribution protocol. A general-purpose OS kernel underneath duplicates much of what the BEAM provides natively. Tyn explores what happens when you remove that redundancy and give BEAM a purpose-built host.
Security. A general-purpose kernel includes subsystems for hardware a cloud BEAM workload never uses — USB, GPUs, dozens of filesystems, thousands of device drivers. Tyn includes only what BEAM needs, reducing the attack surface to a few thousand lines of Rust.
Simplicity. A Tyn image contains only BEAM bytecode and the Rust kernel. No general-purpose OS services, no package management, no user accounts — just your application and its runtime.
Boot speed. Tyn boots in milliseconds, not seconds. For elastic cloud deployments where BEAM nodes scale up and down, this matters.
Density. Tyn images are megabytes, not gigabytes. More BEAM nodes per host, lower cloud costs.
┌─────────────────────────────────────────┐
│ Applications (Elixir / Erlang / Gleam) │
├─────────────────────────────────────────┤
│ OTP / Supervision Trees │
├─────────────────────────────────────────┤
│ ERTS / BEAM VM (unmodified, SMP) │
├─────────────────────────────────────────┤
│ BEAM Host Interface (Rust) │
│ ~50 Linux syscalls emulated │
├─────────────────────────────────────────┤
│ Tyn Kernel (Rust, ~7,000 LOC) │
│ SMP · Memory · Networking · VFS · I/O │
├─────────────────────────────────────────┤
│ KVM / QEMU / Cloud Hypervisor │
└─────────────────────────────────────────┘
Tyn runs the real, unmodified ERTS/BEAM — not a reimplementation. When OTP ships a new version, it should just work. This is the critical lesson from LING (Erlang on Xen), which died because it reimplemented the VM and couldn't keep pace with upstream changes.
OTP 27 BEAM running on bare metal with SMP, TCP, Phoenix + Bandit + Plug, and Elixir.
defmodule TynHelloWeb.HelloController do
use TynHelloWeb, :controller
def index(conn, _params) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello from Phoenix on Tyn!\n")
end
end
defmodule TynHelloWeb.Router do
use TynHelloWeb, :router
pipeline :api, do: plug :accepts, ["json", "html"]
scope "/", TynHelloWeb do
pipe_through :api
get "/", HelloController, :index
end
end
{:ok, _} = Bandit.start_link(plug: TynHelloWeb.Router, port: 8080)$ curl http://localhost:5566/hello
Hello from Phoenix on Tyn!
A TCP eval shell ships alongside the Phoenix demo. Connect from the host and poke a running BEAM:
$ nc localhost 5567
Tyn eval shell - OTP 27, ERTS 15.2.7.1
Expressions end in '.' Disconnect to exit.
>> erlang:system_info(emu_flavor).
jit
>> erlang:system_info(process_count).
351
>> 'Elixir.System':version().
<<"1.18.3">>
>> X = lists:seq(1, 5).
[1,2,3,4,5]
>> lists:sum(X).
15
- OTP 27 ERTS boots with up to 8 CPUs, loads 200+ .beam files from in-memory VFS
- Full OTP kernel application starts — supervision trees, code_server, logger
- Phoenix 1.8 routes through
use TynHelloWeb, :router(Bandit fronts the Phoenix Router directly; the fullPhoenix.Endpointmiddleware stack would also work givensecret_key_baseetc., but the Router is the minimum demo) - Bandit runs unmodified on top of ThousandIsland with the default
num_acceptors: 100configuration — the fullDynamicSupervisor→Connection.start→ handler-spawn chain works - Plug pipeline:
Plug.Conn→put_resp_content_type→send_resp→ host gets the response - Elixir 1.18.3 runs:
IO.puts,System.version,Kernel.inspectall work
Where things stand on KVM (host: AWS Xeon 6975P-C):
-
Image size: 52 MB bootable image (BeamAsm-enabled ERTS) — ~4× smaller than Alpine + Elixir + Phoenix (~190 MB)
-
Cold boot to serving HTTP: ~5 s on KVM with BeamAsm (kernel → BEAM handoff in ~430 ms; the rest is OTP startup plus JIT code-generation for boot modules)
-
Cold-boot reliability with BeamAsm: 60/64 = 93.75 % across a fresh 64-trial sweep on the JIT binary, matching the long-standing non-JIT result. The 4 stalls don't recur in the same place: 3 cluster around the same cold-cache
error_logger.beamload that affects the interpreter, and 1 was a transient BeamAsm "corrupt literal table" rejection oflists.beam— likely a load-time race surfaced by the JIT's stricter validator. No#GP/#PFfaults -
Sustained throughput: 1000 sequential HTTP requests at 14.1 req/s through Phoenix.Router + Bandit + Plug, ~997/1000 OK. Identical rate JIT vs interpreter on the trivial handler — the workload is network-bound. A second
bench_plugexposes/hello,/json,/compute,/fiband lets us measure where compute starts to dominate -
Compute throughput (200 sequential HTTP, Bandit + Erlang bench_plug):
endpoint interpreter req/s JIT (BeamAsm) req/s JIT / interp /hello13.18 12.55 0.95× (parity, network-bound) /json19.80 22.81 1.15× /compute4.91 17.85 3.64× /fib11.78 4.37 0.37× (deep recursion edge case) Pure-compute
timer:tcover the TCP shell isolates BEAM speedup from the network path: JIT is 1.21× faster onfib(28), 1.28× on thefoldlsum, 4.70× faster on a tight list comprehension. The/computeendpoint mirrors that — list-fold work behind Bandit gets the full 3.6× JIT win once network overhead is amortized./hellostays at parity because the workload there is the TCP/smoltcp/virtio round-trip, not BEAM./fib's 25-deep recursion is a synthetic edge case that doesn't reflect real Phoenix handlers (which look like/compute— list ops, map ops, JSON encode) and isn't worth chasing -
Runtime memory: ~400 MB host RSS — ~6× an Alpine container due to ERTS allocator pool defaults (demand paging landed; allocator tuning is next)
- 8-way SMP — ACPI/MADT CPU discovery, APIC timer calibration, AP trampoline (16→64 bit), per-CPU GDT/TSS/IST, GS_BASE per-CPU syscall data, IPI wakeup, preemptive user-mode scheduling
- BeamAsm JIT — OTP 27.3.4.2 with
--enable-jit. The timer trampoline preempts inside mmap'd JIT pages (0x1A00_0000+), and the host-side stat/dir syscalls (newfstatat,S_IFDIRon dir fds, recycledDIR_SLOTS) handle the glibc-style validation the BeamAsm loader does.erlang:system_info(emu_flavor)returnsjit - TCP networking —
gen_tcp:listen/accept/send/closeend-to-end, POSIX socket layer → smoltcp TCP/IP → virtio-net PCI → QEMU → host - Live eval shell —
ncinto a running BEAM and evaluate Erlang or Elixir expressions; bindings persist per session, multi-line input is buffered until the parser accepts it (src/erl/tcp_shell.erl) - Elixir — Elixir 1.18.3 .beam files load and execute on OTP 27
- ~50 Linux syscalls — mmap, read, write, open, stat, pipe, ppoll, futex, clone, epoll, select, readv, ...
- VFS — cpio newc archive with OTP kernel/stdlib .beam files + optional Elixir
- Boot — Multiboot1, identity-mapped 4 GiB, ELF loader for static musl binaries
- Threading — up to 16 CPUs, per-thread kernel stacks, atomic futex, preemptive + deferred scheduling
- I/O — COM1 serial (stdin/stdout/stderr), PCI ECAM, virtio-net
ERTS is built from unmodified OTP 27 source — no patches, no special defines. The build enables BeamAsm and --without-* opts out of unused applications.
Tyn uses a hybrid futex strategy:
- During ERTS init (~first 2 seconds):
futex_waitreturns immediately (spin-yield) to avoid a thread-progress registration deadlock where blocked threads prevent other threads from registering with the progress system. - After init:
futex_waitblocks properly — threads sleep and consume zero CPU until woken byfutex_wake. Idle CPUs enter HLT.
The switch happens automatically after ERTS finishes loading boot modules. Normal operation uses real blocking semantics with proper sleep/wake.
- Boot reliability — JIT now matches the interpreter at ~94 %; remaining ~6 % cluster in code-loader paths (cold-cache
error_logger, occasional BeamAsm "corrupt literal table" rejection oflists.beam) - Concurrent-burst load — sequential 1000/1000 is solid; N≥5 concurrent curls cap at ~2 successful regardless of kernel-side mitigations (verified by exhaustive instrumentation: listener pool, smoltcp, accept logic, ERTS/Bandit all process what arrives). The bottleneck is host-side packet drops at the QEMU TAP / bridge forwarding layer under burst — environmental tuning territory, not kernel work. Realistic concurrent benchmarks need a separate-machine driver instead of host-loopback
- Full IEx — the current eval shell handles single expressions per session; line editing, history, and the real IEx group leader still need stdin I/O server work
- Rust nightly toolchain with
rust-srccomponent - QEMU with KVM support (
qemu-system-x86_64) - A statically-linked
beam.smpand the OTP/Elixir rootfs cpio. Both are committed atsrc/beam.smp.elfandsrc/otp-rootfs.cpioso the kernel builds out of the box. To rebuild them yourself, see "Building ERTS + VFS" below.
cargo build --release --target x86_64-tyn.json \
-Zbuild-std=core,alloc,compiler_builtins \
-Zbuild-std-features=compiler-builtins-memqemu-system-x86_64 \
-kernel target/x86_64-tyn/release/tyn-kernel \
-m 2560M -machine q35 -cpu host -enable-kvm -smp 8 \
-nographic -no-reboot -serial mon:stdio \
-device virtio-net-pci,netdev=net0,disable-legacy=on,disable-modern=off \
-netdev user,id=net0,hostfwd=tcp::5555-:8080,hostfwd=tcp::5567-:9090# In another terminal while QEMU is running, after Tyn prints
# "bandit_listening" on the serial console (~12s after boot):
curl http://localhost:5555/
# → Hello from Bandit on Tyn!Tyn embeds a statically-linked ERTS binary and a cpio archive of .beam files directly in the kernel image. Here's how to build them.
# On an x86_64 Linux host with musl-gcc installed:
git clone --branch OTP-27.3.4.2 https://github.com/erlang/otp.git otp27
cd otp27
# Configure for static musl + BeamAsm
./configure --enable-jit --without-javac --without-odbc --without-wx \
--without-termcap --without-ssl --without-ssh --without-megaco \
--without-diameter --without-observer --without-debugger \
--without-et --without-reltool --without-common-test --without-eunit \
--without-edoc --without-eldap --without-ftp --without-tftp \
--without-snmp --without-docs --without-mnesia \
CC=musl-gcc CFLAGS="-O2 -static" LDFLAGS=-static
# Build
make -j$(nproc)
# The static BeamAsm binary (strip before embedding to fit the layout):
strip bin/x86_64-pc-linux-musl/beam.jit -o beam.smp.elf
ls -lh beam.smp.elf
# → ~10 MB statically linked ELF# Create an OTP release directory with .beam files
mkdir -p staging/otp/bin
cp otp27/bin/start.boot staging/otp/bin/
# Copy kernel and stdlib .beam files (with versioned paths for boot script)
for d in otp27/lib/kernel-*/ebin otp27/lib/stdlib-*/ebin; do
versioned=$(basename $(dirname $d))
mkdir -p staging/otp/lib/$versioned/ebin
cp $d/*.beam staging/otp/lib/$versioned/ebin/
done
# Copy .beam files to root for code_server fallback loading
cp otp27/lib/kernel-*/ebin/*.beam staging/
cp otp27/lib/stdlib-*/ebin/*.beam staging/
# Create the cpio archive
cd staging
find . -type f | sed 's|^\./||' | cpio -o -H newc > ../src/otp-rootfs.cpio
# Copy the (stripped) BeamAsm ERTS binary
strip otp27/bin/x86_64-pc-linux-musl/beam.jit -o ../src/beam.smp.elf# Download prebuilt Elixir for OTP 27
curl -L -o elixir.zip \
https://github.com/elixir-lang/elixir/releases/download/v1.18.3/elixir-otp-27.zip
unzip elixir.zip -d elixir
# Add Elixir .beam files to the staging root
cp elixir/lib/elixir/ebin/*.beam staging/
cp elixir/lib/iex/ebin/*.beam staging/
# Rebuild cpio with Elixir included
cd staging && find . -type f | sed 's|^\./||' | cpio -o -H newc > ../src/otp-rootfs.cpio- Module structure — source file dependencies and line counts
- Boot flow — from power-on to Erlang shell, syscall sequence
- Runtime architecture — CPU layout, futex strategy, memory map
These document the bug-class hunts that got Tyn from "ERTS boots" to "Bandit + Plug serves real traffic":
- BOOT_RELIABILITY.md — failure modes, stack-layout trace through preemption + syscall, what fixes worked and why
- MESSAGE_DELIVERY.md — scheduler-wake / process-scheduling races, the watchdog-rescue fix, and the
sys_acceptrace that was blocking ThousandIsland's concurrent-acceptor pattern (now fixed)
Run the real BEAM. Not a reimplementation — the actual ERTS, cross-compiled for Tyn's host interface.
Purpose-built for BEAM. The kernel hosts one runtime and nothing else. This constraint enables a small trusted computing base and a clean verification story.
Minimal kernel, maximal BEAM. The kernel provides only what BEAM needs — memory, interrupts, device access, network. BEAM handles its own scheduling, memory management, code loading, and supervision.
Target KVM/virtio. Standardized virtual hardware means the kernel only needs a handful of drivers. The entire device layer is a few hundred lines of Rust.
Designed for verification. The kernel is structured for future formal verification with Verus. Minimal unsafe code, explicit invariants, small trusted computing base.
- LING — Erlang on Xen. Proved the concept. Died because it reimplemented BEAM and targeted only Xen.
- Nerves — Elixir on embedded Linux. Complementary — Nerves owns embedded, Tyn targets cloud.
- GRiSP — BEAM on RTEMS for IoT hardware. Different niche.
- Asterinas — Rust Linux-compatible kernel. Architectural reference.
- rcore-os/virtio-drivers — VirtIO drivers used by Tyn.
- smoltcp — TCP/IP stack used by Tyn.
Tyn is part of a broader ecosystem:
- Vor — A BEAM-native language with compile-time verification
- VorDB — A CRDT-based distributed database built on Vor
MIT OR Apache-2.0