Running Wasm Modules with the wasmtime CLI
This guide covers the handful of wasmtime commands that turn a .wasm file into something you can
actually interrogate: executing a command module, calling a single export with literal arguments,
passing arguments and environment through, reading a trap properly, and turning on the proposals a
module needs.
The reason to learn these before reaching for a browser is feedback speed. A browser round trip means
a build, a server, a reload and a console; wasmtime run is one command and prints a symbolic trap
with a backtrace. For anything that is a logic question rather than a loading question, the command
line is where you want to be.
Prerequisites
- [ ]
wasmtime20 or newer (wasmtime --version) - [ ] A
.wasmfile — a WASI command, or any module with exports you want to call - [ ]
wasm-objdumpfromwabt, to look up the exact export names - [ ] A shell where you can set environment variables for one command
Step-by-step procedure
1. Run a command module
wasmtime run tool.wasm
If the module has a _start export this runs it and exits with the program’s status. Anything the
program writes to standard output appears directly; anything it writes to standard error does too.
2. Pass arguments and environment
Everything after -- belongs to the module, not to wasmtime:
wasmtime run tool.wasm -- --input report.csv --limit 500
wasmtime run --env LOG=debug --env TZ=UTC tool.wasm
The module sees the arguments through the normal WASI args_get path, so std::env::args in Rust or
argv in C behaves as it would natively. Note that the environment is not inherited by default,
which is deliberate: a WASI module gets only what you pass.
3. Call one export directly
wasm-objdump -x lib.wasm | grep '^ - func' | head # find the exact name
wasmtime run --invoke add lib.wasm 2 3
Arguments are parsed as the export’s declared parameter types. This path is ideal for a quick check of a pure function and useless for anything taking a pointer, because there is no way to populate memory first.
4. Grant filesystem access
wasmtime run --dir ./fixtures::/data tool.wasm -- --input /data/sample.bin
The syntax is host-path::guest-path. Inside the module, only the guest path exists. Omitting this
mapping is the cause of most “file not found” reports from programs that work natively.
5. Read a trap properly
When the module traps, wasmtime prints the category and a backtrace. Ask for the detailed version:
WASMTIME_BACKTRACE_DETAILS=1 wasmtime run tool.wasm
6. Enable the proposals the module uses
A module built with post-MVP features needs them enabled on an older runtime:
wasmtime run -W simd,threads,bulk-memory tool.wasm
wasmtime --version # check first — recent versions enable the stable set by default
Expected output
A clean run and a trapping run look like this. The second is the more useful one to recognise:
$ wasmtime run tool.wasm -- --input /data/sample.bin
read 4096 bytes
checksum 0x8f31a2b4
$ WASMTIME_BACKTRACE_DETAILS=1 wasmtime run tool.wasm -- --input /data/bad.bin
Error: failed to run main module `tool.wasm`
Caused by:
0: failed to invoke command default
1: error while executing at wasm backtrace:
0: 0x1a4c - tool::decode::h9c1
1: 0x2f10 - tool::main::h4ab
2: wasm trap: out of bounds memory access
Three things are worth reading in that output. The trap category — out of bounds memory access —
names the class of bug. The backtrace names the function, which is only possible because the binary
still has its name section. And the offsets let you find the exact instruction if you need to, by
decoding forward from the function body.
Gotchas
error: failed to find function export '_start'. The module is a library, not a command. Use
--invoke <name> with an export from wasm-objdump, or build the crate as a binary.
Arguments end up in wasmtime rather than the module. They were placed before --. Everything
intended for the module goes after it; everything before it configures the runtime.
Environment variables are missing inside the module. WASI does not inherit the shell environment.
Pass each one with --env, or --env-inherit where the runtime supports it and the exposure is
acceptable.
unknown import for something that is not WASI. The module expects host functions this runtime
does not provide — typically a browser-targeted build with wasm-bindgen glue imports. Those need the
JavaScript glue to run; the CLI cannot supply them.
Validation fails for a feature you know is standard. The runtime predates it. Check
wasmtime --version and enable the proposal explicitly with -W, or upgrade.
Performance note
Cold start under wasmtime for a small module is a few milliseconds, dominated by compilation rather
than by instantiation. That makes the CLI fast enough for a tight edit-run loop, but it also means
repeated invocation in a shell loop measures the compiler more than your code. For repeated runs,
either use --invoke on a resident module through an embedding, or cache the compiled artifact with
wasmtime compile, which produces a precompiled file the runtime loads without compiling again.
Absolute throughput numbers from the CLI are a relative signal only. The optimising tier here is not
the one a browser uses, so a kernel that is 20% faster under wasmtime may be 5% slower in Chrome.
Use the CLI to answer “did this change help”, and a browser to answer “how fast is it for users”.
Precompiling for repeated runs
When a module is invoked many times — a linter in a file-watching loop, a request handler in a local
harness — the compilation cost is paid on every start. wasmtime compile removes it by producing a
precompiled artifact the runtime can load directly:
wasmtime compile tool.wasm -o tool.cwasm
wasmtime run --allow-precompiled tool.cwasm -- --input /data/sample.bin
The precompiled file is specific to the runtime version and the host architecture, so it is a build
output rather than something to distribute — the same constraint that applies to a cached compiled
module in a browser. Treat it as a local accelerator: generate it in the same script that builds the
.wasm, keep it out of version control, and regenerate it whenever either the module or the runtime
changes.
For a comparison across runs, the saving is easy to see. A module that takes 40 ms to compile and 2 ms
to instantiate starts in roughly 42 ms from the .wasm and about 4 ms from the .cwasm, which over a
few hundred invocations in a watch loop is the difference between a perceptible pause and none.
Using the CLI as a test harness
Because --invoke calls a single export and prints its return value, a shell script is often the
fastest way to build a regression test for a numeric module — no JavaScript, no test framework, no
build step beyond the one producing the binary:
#!/usr/bin/env bash
set -euo pipefail
check() {
local expected="$1"; shift
local actual
actual=$(wasmtime run --invoke "$@" module.wasm 2>/dev/null | tail -1)
[ "$actual" = "$expected" ] || { echo "FAIL: $* → $actual (expected $expected)"; exit 1; }
}
check 5 add 2 3
check 0 add 0 0
check -1 add 2 -3
echo "all checks passed"
That covers pure functions well and stops at anything needing memory, which is the natural boundary of
the CLI’s usefulness. Once a test needs to write bytes into linear memory first, a short Node script
is the smaller tool — it can populate memory, call the export, and read the result back, all in a
dozen lines.
The division that works in practice is: the CLI for anything you can express as scalars, Node for anything involving buffers, and a browser only for the load path. Each step up costs more setup, so staying on the lowest rung that answers the question keeps the feedback loop short.
Frequently Asked Questions
Can I debug a module with a real debugger?
Yes — wasmtime run -D debug-info keeps DWARF available and lets gdb or lldb attach, which is
worth setting up for a module you will be debugging repeatedly. For one-off questions the backtrace
above is usually enough.
How do I see what a module is doing to the filesystem? Grant only the directory it should touch and watch what fails. A module that reaches outside its preopen produces a clear error rather than silently succeeding, which makes the CLI a good place to audit path handling before deployment.
Does wasmtime support threads?
Shared memory and atomics are available behind the threads proposal, but a WASI command has no way to
spawn a thread by itself in preview 1 — thread creation is the host’s job. For browser threading, see
SharedArrayBuffer, Atomics & threading.
Related
- WASI target builds & runtimes — producing the modules this CLI runs.
- Granting filesystem access with WASI preopens — the
--dirmapping in depth. - Setting up a local Wasm runtime for testing — the same loop with Node as the host.
- Decoding Wasm opcodes for debugging — turning a trap offset into an instruction.
← Back to WASI Target Builds & Runtimes