Wasmex: running WebAssembly inside an Elixir backend
Execute WebAssembly from Elixir
At a glance
- What is it?
- Wasmex is an Elixir library that runs WebAssembly and WASI modules through wasmtime, exposed to the BEAM through a Rust NIF. The idea is to let a Phoenix backend execute user supplied or cross language logic in a sandboxed runtime, at the cost of a native dependency and a per instance process model.
- Who is it for?
- Adopt Wasmex if your Elixir service needs to run untrusted or third party code in a sandbox, or needs one compiled artifact to serve both a JavaScript frontend and an Elixir backend without two implementations of the same logic. Do not adopt it if you need a fully managed deployment, since the NIF is a native dependency compiled and linked into the BEAM, or if your workload is a long running compute job where a dedicated service would be a better boundary.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
wasmtime behind a NIF, one Wasm instance per process
Wasmex is described as a fast and secure WebAssembly and WASI runtime for Elixir, and its purpose is to enable lightweight WebAssembly containers to run inside an Elixir backend.
The mechanism is specific. Wasmex uses wasmtime to execute Wasm binaries through a Rust NIF, so the runtime lives in native code and the BEAM talks to it across the NIF boundary. The library is written in Elixir on top of that, and the repository is a mix project with the Rust side in a native directory alongside a mix.exs, a mix.lock, a lib directory and a test directory. The primary language of the project is Rust, which tells you where the effort sits.
The execution model follows from that. Starting a module gives you a pid for a GenServer that is running a Wasm instance, and calls go through that pid:
bytes =
File.read!(
"test/fixture_projects/wasm_test/target/wasm32-unknown-unknown/debug/wasmex_test.wasm"
)
{:ok, pid} = Wasmex.start_link(%{bytes: bytes}) # starts a GenServer running a Wasm instance
{:ok, [42]} == Wasmex.call_function(pid, "sum", [50, -8])So the unit of isolation is a process wrapping an instance, and functions are called by name with a list of arguments. That shape fits the BEAM well, because each instance can be supervised like any other process, and a crash in one module does not take down the caller.
The instance is a live Wasm runtime rather than a one-shot call, which is what makes stateful modules possible at all, and also what makes resource management your responsibility.
The two use cases the sandbox actually unlocks
The readme lists what it calls the good use cases, and they are worth separating because they have different requirements.
The first is running user provided code safely. WebAssembly describes a memory-safe, sandboxed execution environment, and that property is exactly what a platform feature for plugins, user scripts or uploaded algorithms needs. Running untrusted code inside the BEAM itself would be a different and much worse proposition, so a Wasm sandbox is the credible way to let strangers extend your service.
The second is sharing business logic between systems written in different languages, with a JavaScript frontend and an Elixir backend given as the example. Compile the rules once to Wasm and both sides run the same artifact. That is a real alternative to maintaining two implementations of a pricing or validation rule, and it is the use case that does not depend on the sandbox at all.
A third case is running existing libraries and programs and interfacing them from Elixir, which covers the long tail of a format converter or a codec that already exists as C or Rust but has no Elixir port.
The framing in the readme is that Wasm on an Elixir host is great not only but especially for these, and the word especially is doing work: the interesting case is the sandbox one, and that also happens to be the case with the sharpest operational edges.
WASI support widens the surface beyond bare functions
The runtime handles WASI as well as WebAssembly, and the difference shows up in the development tooling the project expects.
For a plain Wasm module, you compile to wasm32-unknown-unknown, and that is the target the example fixture in the repository uses, at test/fixture_projects/wasm_test/src/lib.rs. The example is deliberately trivial, a single exported function:
#[no_mangle]
pub extern fn sum(x: i32, y: i32) -> i32 {
x + y
}The point of that fixture is that it can be written in any language that compiles to WebAssembly, so a team porting existing logic does not have to learn Rust to use Wasmex.
The wider set of targets in the toolchain list shows what else is supported. wasm32-unknown-unknown covers the plain module, wasm32-wasip1 covers WASI files for testing, and wasm32-wasip2 is for component resource fixtures, with cargo install --locked cargo-component for the Wasm component model. WASI is what gives a module a filesystem, standard streams and clocks, so a module that was a pure calculation becomes a program that can read input and produce output, and the component model targets point toward a future where modules are composed with typed interfaces rather than raw exports.
That progression is visible in the project's own tooling rather than in its marketing, which is a good sign, but it also means the more advanced targets are exercised mainly by fixtures.
Building from source is a Rust toolchain plus an environment flag
The published package is consumed as an ordinary Elixir dependency, which is the easy path:
def deps do
[
{:wasmex, "~> 0.15.1"}
]
endDeveloping on the project itself is where the Rust side appears. The setup is a set of rustup commands that add the formatter and linter components and the three compilation targets, plus one cargo install for the component model tooling:
rustup component add rustfmt
rustup component add clippy
rustup target add wasm32-unknown-unknown # to compile our example Wasm files for testing
rustup target add wasm32-wasip1 # to compile our example Wasm/WASI files for testing
rustup target add wasm32-wasip2 # to compile component resource fixtures
cargo install --locked cargo-component # for Wasm component model filesThen the Erlang and Elixir dependencies, with the note that this assumes you installed Elixir and Erlang through asdf, and otherwise you install them your own way:
asdf install # assuming you install elixir, erlang with asdf. if not, make sure to install them your way
mix deps.getThe detail that catches contributors out is an environment variable. If you change anything on the Rust side of the project, you set WASMEX_BUILD=true so your changes are picked up, and the integration suite is run the same way with WASMEX_BUILD=true mix test. The Rust unit tests live in their own manifest and run with cargo test against native/wasmex/Cargo.toml with all targets and all features. The project also expects formatters and linters to pass, cargo fmt, cargo clippy, mix format and mix credo, which is a reasonable quality bar for a project with a native component.
The practical consequence is that a hex dependency and a git checkout behave differently, and the difference is that a git checkout compiles the Rust NIF locally.
What the model costs you at run time
There are three costs that the readme does not spell out and that you will meet in production.
The first is the NIF boundary. Every call into a Wasm function crosses from the BEAM into native code and back, and native calls block the scheduler thread they run on. A long running Wasm function therefore holds a scheduler thread for its whole duration, which is the opposite of what an Elixir developer expects from a process. The library describes itself as fast, but the numbers that matter depend on the module, the function and your release build, so benchmark rather than trust the adjective.
The second is that each instance is a process with a lifetime. Wasmex.start_link returns a pid, which means an instance is something you supervise, monitor and shut down. Under a supervision tree this is comfortable. Outside one, a leaked instance is leaked memory, because a Wasm instance holds its own linear memory outside the BEAM heap and the garbage collector does not know about it.
The third is that a Wasm module is not a native library. It has its own linear memory, its own imports and its own constraints, and what it can do is bounded by what the host wires up. If your module needs to call back into your application, that is an import you have to provide, and the surface you expose is part of your security boundary.
None of these make Wasmex a bad choice. They make it a choice about where the boundary goes: a Wasm instance in process per module is a clean boundary, while a Wasm instance per request in a GenServer pool is a much weaker one.
Where this sits against the alternatives
The realistic alternatives for the same job differ in what they treat as the unit of isolation.
Running the logic in a native NIF written in Rust is the closest alternative and the fastest, and it is the obvious choice when the code is yours and trusted. The difference is that a native library runs with the full privileges of the BEAM process, so a bug in it takes down the VM rather than one process, and it cannot be handed to a user as a plugin.
Running a separate service is the other alternative, communicating over HTTP or a message queue. It gives you a process boundary that is stronger than a NIF and a deployment unit you can scale and restart independently, at the cost of a network hop, a second runtime to operate and, for the BEAM, losing the cheap supervision model.
Running the code in a container is a third option, similar in isolation properties to Wasm but heavier to start and to move data in and out of.
Wasmex sits between the NIF and the sidecar, and it wins where the code is untrusted or shared between languages, because the sandbox comes from the Wasm memory model rather than from a deployment boundary you have to build. The case where it loses is when the module is trusted, hot, and latency sensitive, where a plain NIF with no marshalling is strictly better.
Editorial conclusion
Adopt Wasmex if your Elixir service needs to run untrusted or third party code in a sandbox, or needs one compiled artifact to serve both a JavaScript frontend and an Elixir backend without two implementations of the same logic. Do not adopt it if you need a fully managed deployment, since the NIF is a native dependency compiled and linked into the BEAM, or if your workload is a long running compute job where a dedicated service would be a better boundary. Before you commit, benchmark a call in your own release build, because the NIF and its instance model add overhead that a hello world will not show, and decide deliberately whether each module gets its own instance and process.
Frequently asked questions
How do I install Wasmex in an Elixir project?
Add wasmex to the dependencies in mix.exs with the version constraint {:wasmex, "~> 0.15.1"} and fetch it. The published package ships the prebuilt native side, while a git checkout compiles the Rust NIF locally.
How do I call a function in a WebAssembly module from Elixir?
Start an instance with Wasmex.start_link, which returns a pid for a GenServer running a Wasm instance, then call Wasmex.call_function with that pid, the function name and a list of arguments. The example in the project calls a sum function and expects {:ok, [42]} for the arguments 50 and -8.
What runtime does Wasmex use to execute Wasm binaries?
Wasmex uses wasmtime to execute Wasm binaries through a Rust NIF, so the WebAssembly and WASI runtime is native code called from the BEAM. The primary language of the project is Rust.
How do I run the Wasmex test suites?
The Elixir integration suite runs with WASMEX_BUILD=true mix test, and the Rust unit tests run with cargo test against native/wasmex/Cargo.toml with all targets and all features. The formatters and linters are cargo fmt, cargo clippy, mix format and mix credo.
What are the main use cases for running WebAssembly from an Elixir backend?
The project names running user provided code safely, sharing business logic between systems in different languages such as a JavaScript frontend and an Elixir backend, and running existing libraries or programs from Elixir.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tessi-wasmex)Was this useful?