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.

Two ways to start a module Plain run executes the module's start function, which is what a command built from a binary crate expects. Invoke calls one named export with literal arguments and prints its return value, which suits a library-shaped module or a single function under investigation. wasmtime run tool.wasm calls the start function arguments after -- reach std::env::args exit status is the program's for anything with a main wasmtime run --invoke add m.wasm 2 3 calls one export by name arguments are literal i32 / i64 / f64 values prints the return value for a single function under investigation Only scalars can be passed with --invoke: anything that needs a pointer needs a host that can write into linear memory first. For those, a short Node script is the smaller tool — see the local runtime guide.

Prerequisites

  • [ ] wasmtime 20 or newer (wasmtime --version)
  • [ ] A .wasm file — a WASI command, or any module with exports you want to call
  • [ ] wasm-objdump from wabt, 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.

What a trap report tells you The innermost cause names the trap category. The backtrace names the functions, which requires the name section to have survived stripping. The offsets identify the exact instruction for a manual decode when the function name is not enough. the category out of bounds · unreachable · divide by zero · type mismatch tells you what kind of bug the backtrace function names, innermost first needs the name section tells you where the offsets byte positions in the code section decode forward to find the instruction tells you exactly which one Strip the name section and the middle box disappears — which is why release artifacts you may have to debug should keep it.

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.

Which side of the double dash Flags placed before the double dash are read by wasmtime and configure the sandbox, the environment and the enabled proposals. Everything after it is passed through untouched and arrives in the module's own argument list. wasmtime reads these --dir · --env · -W simd · --invoke -- the module reads these --input /data/x.bin · --verbose A flag on the wrong side either configures something you did not mean to, or reaches a program that does not recognise it. When a module reports an unknown argument, check the separator before checking the parser.

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.

← Back to WASI Target Builds & Runtimes