-
Pl
chevron_right
ProcessOne: Nera - Erlang that ships like Go
news.movim.eu / PlanetJabber • 16:00 • 9 minutes

We have been shipping Erlang for more than twenty years. ejabberd runs on the BEAM, and the BEAM has earned its reputation: lightweight processes, isolation, supervision, per-process garbage collection. Few runtimes have aged this well.
And yet, every time we deploy an Erlang program, we deploy two things. The program, and the virtual machine it needs to run.
For a large messaging server, that trade-off is obvious. For a command-line tool, a single-purpose service or an agent at the edge of a network, it is much harder to justify. This is where many teams pick Go. Not because goroutines are a better concurrency model than Erlang processes. Because go build gives them one file they can copy to a machine and run.
What if Erlang could do the same?
That is the question behind Nera, and its repository is now public: github.com/processone/nera. Nera v0.1.0 is out. It is a full release, and it stays experimental until 1.0. This post says exactly what that means.
A native binary with an Erlang runtime inside
Nera is an ahead-of-time compiler for Erlang, written in Rust. It parses .erl source files, lowers them to an intermediate representation, generates machine code with Cranelift, and links the result with Nera&aposs runtime into a single executable.
.erl source → parse → Nera IR → Cranelift → link with the runtime → native executable
There is no bytecode, no interpreter and no JIT. The output is a regular native program, built for one operating system and architecture like any other. The machine that runs it needs no Erlang installation, no release tooling and no erl.
Compiling Erlang to machine code is only half of the problem, though. Erlang programs expect processes, mailboxes and a scheduler. On the BEAM, the virtual machine provides them. With Nera, a runtime written in Rust takes over that part of the VM&aposs job, and it is linked into every program the compiler produces.
That runtime provides:
- lightweight Erlang processes, message passing with copy semantics, and selective
receivewith timeouts; - links, monitors and trapped exits, the building blocks of supervision;
- per-process generational garbage collection, so there is no global stop-the-world pause;
- a work-stealing scheduler with one thread per CPU core and reduction-based preemption;
- timers, ETS tables,
persistent_term,counters,atomics, and TCP sockets throughgen_tcp.
On top of it, Nera compiles a standard library alongside your code: lists and maps, plus subsets of gen_server, supervisor, application and proc_lib. Their sources are embedded in the compiler binary, so there is nothing else to install.
This is a part of the BEAM, not all of it. Nera does not do hot code upgrades. It does not do distributed Erlang. It has no TLS yet. It is not a drop-in replacement for the BEAM, and it is not trying to be one.
Step by step: two processes playing ping-pong
What does this look like in practice? The repository ships a classic example: two Erlang processes exchanging messages. Here it is, from source to a running native program. Every output below comes from running these commands on macOS arm64 against the public repository.
1. Build the compiler. Nera needs Rust 1.99.0, pinned in the repository, and a C linker:
git clone https://github.com/processone/nera.git
cd nera
cargo build --release --locked
This produces the compiler, target/release/nera, and the runtime library it links into every program, target/release/libnera_runtime.a.
2. Read the program. examples/ping_pong.erl spawns a second process, sends it ten pings, and waits for a pong after each one:
-module(ping_pong).
-export([start/0, pong/0]).
start() ->
Pong = spawn(ping_pong, pong, []),
ping(Pong, 10).
ping(_, 0) ->
io:format("Ping done~n");
ping(Pong, N) ->
Pong ! {ping, self()},
receive
pong ->
io:format("Ping received pong (~w remaining)~n", [N]),
ping(Pong, N - 1)
end.
pong() ->
receive
{ping, From} ->
io:format("Pong received ping~n"),
From ! pong,
pong()
end.
Nothing here is specific to Nera. It is plain Erlang: spawn, ! and receive.
3. Compile it to a native executable.
./target/release/nera build examples/ping_pong.erl -o ping_pong -e ping_pong:start
-o names the output file and -e the entry point, the function the program starts with. The compiler prints one line naming the runtime library it links, and writes ping_pong in the current folder.
4. Look at what it produced.
file ping_pong
otool -L ping_pong
ping_pong: Mach-O 64-bit executable arm64
ping_pong:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
A regular macOS executable, whose only dynamic dependency is the system library. No BEAM, no erl, no release directory next to it.
5. Run it.
./ping_pong
Pong received ping
Ping received pong (10 remaining)
Pong received ping
Ping received pong (9 remaining)
Pong received ping
Ping received pong (8 remaining)
Pong received ping
Ping received pong (7 remaining)
Pong received ping
Ping received pong (6 remaining)
Pong received ping
Ping received pong (5 remaining)
Pong received ping
Ping received pong (4 remaining)
Pong received ping
Ping received pong (3 remaining)
Pong received ping
Ping received pong (2 remaining)
Pong received ping
Ping received pong (1 remaining)
Ping done
The program exits with status 0 when start/0 returns. The same source, compiled with erlc and run on OTP 29, prints the same 21 lines.
The two processes are scheduled by Nera&aposs runtime, which starts one scheduler thread per CPU core by default. Setting NERA_SCHEDULERS=1 when you launch the program runs it on a single thread, which is handy for reproducible tests.
More than a compiler
A compiler is only usable with the tools around it. Nera manages dependencies from hex.pm, git or a local path through a nera.toml file, and it can also build against the dependencies rebar3 has already fetched. It has a test runner (nera test), a formatter (nera fmt), and an LSP server with a VS Code extension and a JetBrains plugin, both attached to the release (the JetBrains plugin needs a commercial IDE, version 2025.3 or later). An Emacs integration is in the repository.
The checker is useful even if you never deploy a Nera binary. nera check runs the whole front end, from parsing and preprocessing to lowering and validation, without generating code. It understands standard OTP project layouts and rebar3 dependency directories, so you can point it at an existing project and get diagnostics in your editor while you keep building and deploying with the standard Erlang/OTP toolchain. (Its warnings can still differ from erlc&aposs. We want to hear about every false one.)
What runs today
A ping-pong between two processes proves very little on its own. So we test Nera against real libraries.
The most complete result so far is Cowlib, the protocol library behind the Cowboy HTTP server. The repository includes a small project, examples/cowlib_demo, that fetches Cowlib 2.22.0 from hex.pm and builds it natively. With target/release on your PATH:
cd examples/cowlib_demo
nera deps get
nera build
./cowlib_demo
It makes 85 calls across all 25 Cowlib modules: cookies, dates, URIs, HTTP headers, HTTP/2 and HTTP/3 framing, HPACK and QPACK, multipart, WebSocket masking. All 85 return without an exception. Run against the same code on OTP 29, 75 output lines are byte-identical. Eight hold the same values with a different map order or an atom printed without quotes. Two differ only in byte order, because Nera orders atom keys differently from OTP; they are equal once sorted. Cowlib&aposs upstream cookie and date suites ran natively on an earlier version (2.20.0); we have not rerun them on 2.22.0.
Thoas 1.2.1, a JSON library, passes its recorded checks on Nera, including all 318 cases of JSONTestSuite. And on macOS arm64, Nera&aposs main branch passes its local gate with 2,118 Rust workspace tests and 450 Erlang tests.
What does not work yet
Experimental is not a polite word for "almost done". Here is where Nera stands, taken from the known limits page we keep in the repository and revise with every release:
- Platforms. Nera is developed on macOS arm64. Linux x86-64 is built and tested natively in CI, and the release job compiles and runs a program from the installed archive. The macOS x86-64 and Linux arm64 archives are cross-built and have never been executed. Windows is not supported: the compiler does not link there and there is no archive.
- Cowboy does not run yet. Nera lowers all 68 modules of Cowboy 2.20.0, Ranch 1.8.1 and Cowlib 2.21.0, but they still call about 70 OTP functions that Nera does not implement. A native "hello world" server links, then never opens a socket.
- No TLS, no distribution, no hot code upgrade. The
sslmodule is not implemented, Nera runs single-node programs, and code cannot be reloaded at run time. - Partial OTP. Supervisors honour only
one_for_one. Some ETS andbinaryfunctions are missing. After agen_server:calltimes out, the late reply still arrives in the caller&aposs mailbox. - Known wrong results. Some operations on terms nested thousands of levels deep crash on the native stack, where the BEAM handles them. A few printing details differ.
- Performance. The benchmark figures in the README are historical, measured on one Apple M4 Pro, and have not been revalidated. We will not quote them as results until we rerun them.
In short: do not use Nera for production services yet. There is no compatibility promise between releases, and wrong results are still being found. If an output matters, compare it with the BEAM.
Why we are building it
Let me be clear about one thing first. We are not moving ejabberd to Nera. ejabberd relies on hot code loading, distribution, Mnesia and native extensions. It belongs on the BEAM, and it stays there.
What interests us is everything around it.
Erlang&aposs ideas are worth more than any single runtime: isolated processes, message passing, supervision, and letting a failing process crash. Today, using them means accepting the BEAM&aposs deployment model. For a lot of software, that is the right deal. For a lot of other software, it is the reason Erlang was never considered in the first place.
The first demonstrator on our roadmap makes this concrete for messaging. It is an external XMPP component, connected to an existing server such as ejabberd through XEP-0114, that routes alerts with Erlang rules and runs as a native executable on a host without Erlang. The same plan adds compile-time checking of the contracts in its Erlang core. Both are planned work, not results, and the roadmap lists the gates each must pass.
What comes next
Nera v0.1.0 is out. Later experimental tags will be named vX.Y.Z-experimental and published as GitHub prereleases. Next, the next milestone is Cowboy over plain HTTP, then Cowboy with TLS.
Those are milestones, not dates.
Try it, and tell us what breaks
Download the archive for your platform from the v0.1.0 release page, or build from source as shown above. The ping-pong steps above are the shortest way in. From there, the walkthroughs for Erlang developers go through processes, an OTP-style server, a real JSON library and the editor tooling. Read the known limits before porting anything larger.
When Nera gives a different result from the BEAM, the most useful report is a single .erl file with the output of both. Open an issue on GitHub. Nera is released under the Apache 2.0 license.
Erlang&aposs ideas deserve to run in more places than the BEAM can reach. Nera is our bet that they can, but it is an early attempt.