Running Components in wasmtime
This page answers one task: you have a WebAssembly component — a command-line tool, an HTTP handler, or a library exporting a WIT interface — and you want to run it with wasmtime, either from the command line or embedded in a Rust host application.
Prerequisites
- [ ] wasmtime installed (
curl https://wasmtime.dev/install.sh -sSf | bash) for the CLI. - [ ] For embedding, a Rust project with the
wasmtimeandwasmtime-wasicrates, matching versions. - [ ] A component, for example from building a component in Rust with cargo-component.
Three kinds of component, three ways to run
How a component runs depends on the world it implements. A command component exports wasi:cli/run — it has a main function, reads arguments and
environment, writes to stdout — and wasmtime run executes it like a program. An HTTP component exports wasi:http/incoming-handler, and wasmtime serve turns it into an HTTP server. A library component exports a custom interface, such as example:text/slug; there is nothing to “run” from the
command line, so a host program embeds wasmtime, instantiates the component and calls its exports with typed arguments.
The same engine handles all three. The difference is which world the host expects and which imports it provides — WASI interfaces for commands and HTTP handlers, plus anything custom for libraries.
Step 1 — run a command component
wasmtime run tool.wasm -- input.txt --verbose
wasmtime run --dir ./data::/data tool.wasm -- /data/input.txt
wasmtime run --env RUST_LOG=debug tool.wasm
--dir host::guest preopens a host directory at a guest path — without it, the component has no filesystem access at all. --env passes environment
variables explicitly; nothing is inherited by default. Resource limits are flags too: -W max-memory-size=268435456 caps linear memory, and
-W fuel=… or timeouts bound execution. The CLI detects whether the file is a component or a core module and runs either.
Step 2 — serve an HTTP component
wasmtime serve --addr 127.0.0.1:8080 handler.wasm
curl http://127.0.0.1:8080/hello
Each incoming request instantiates the component and calls its handler, so instances are isolated from each other and state does not leak between
requests. -S cli grants the handler access to WASI CLI interfaces such as environment variables, which many handlers use for configuration. The full
workflow is in
handling HTTP requests with wasi:http.
Step 3 — embed a library component in Rust
For a component exporting a custom interface, generate host-side bindings from the same WIT with wasmtime::component::bindgen!:
use wasmtime::component::{bindgen, Component, Linker, ResourceTable};
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
bindgen!({ world: "slugger", path: "../slugify/wit" });
struct Host { wasi: WasiCtx, table: ResourceTable }
impl WasiView for Host {
fn ctx(&mut self) -> &mut WasiCtx { &mut self.wasi }
fn table(&mut self) -> &mut ResourceTable { &mut self.table }
}
fn main() -> anyhow::Result<()> {
let engine = Engine::new(Config::new().wasm_component_model(true))?;
let component = Component::from_file(&engine, "slugify.wasm")?;
let mut linker = Linker::<Host>::new(&engine);
wasmtime_wasi::add_to_linker_sync(&mut linker)?; // satisfy any WASI imports
let host = Host { wasi: WasiCtxBuilder::new().inherit_stderr().build(), table: ResourceTable::new() };
let mut store = Store::new(&engine, host);
let slugger = Slugger::instantiate(&mut store, &component, &linker)?;
let opts = exports::example::text::slug::Options { max_length: Some(40), separator: None };
let s = slugger.example_text_slug().call_slugify(&mut store, "Hello, Component Model!", &opts)?;
println!("{s}"); // hello-component-model
Ok(())
}
bindgen! generates a Slugger type with typed methods for each export — call_slugify takes a &str and the Options struct and returns a String.
The host never sees memory or pointers. Exact module paths of the WASI helpers vary between wasmtime versions; consult the version’s documentation.
Keep the Engine and the compiled Component in long-lived application state, and create the Linker once as well — it only holds definitions, not
per-instance data. Only the Store and the instance are per call or per tenant.
Step 4 — provide custom imports
If the component imports an interface — say example:logging/log — the host implements the generated trait and adds it to the linker:
impl example::logging::log::Host for Host {
fn info(&mut self, msg: String) { println!("[guest] {msg}"); }
}
example::logging::log::add_to_linker(&mut linker, |h: &mut Host| h)?;
The guest’s calls then arrive as ordinary Rust method calls on the host state. This is how plugin hosts expose capabilities — and only those capabilities — to guests, the pattern described in sandboxing third-party code with Wasm.
Step 5 — reuse compiled components and limit resources
Compiling a component is expensive; instantiating it is cheap. Compile once per engine and instantiate per request or per tenant. For production,
precompile ahead of time with wasmtime compile or Engine::precompile_component and load with Component::deserialize, which skips compilation
entirely at startup. Bound each store’s memory with Store::limiter, and CPU time with fuel (Config::consume_fuel) or epoch interruption
(Config::epoch_interruption), so a misbehaving guest cannot exhaust the host. Async hosts enable Config::async_support and use the _async variants of
the WASI and bindgen APIs, which lets a guest’s blocking I/O suspend without blocking a thread.
Handling errors from guest calls
Every typed call returns a wasmtime::Result, and the error covers several distinct situations that a host should treat differently. A guest that
returns a WIT result<T, E> with an error value is behaving normally: the outer Result is Ok, and the inner one carries the guest’s error, which the
host handles as domain logic. A trap — an unreachable from a Rust panic, an out-of-bounds access, a stack overflow — arrives as an outer Err whose
downcast_ref::<wasmtime::Trap>() identifies the kind; the instance must then be considered unusable, exactly as in the browser case described in
recovering a module after a trap.
Running out of fuel or hitting an epoch deadline also produces a trap, of a specific kind, which a host usually reports as a timeout. And a failure inside
a host import — a database error in the host’s implementation of an interface — propagates out through the guest call unless the host maps it to a WIT
error value. Distinguishing these cases in the host’s error handling keeps logs meaningful and prevents a single misbehaving guest from looking like a
host bug.
Choosing between the CLI and embedding
The CLI is the right tool for running tools and trying components out, and wasmtime serve is a capable development server for HTTP handlers. Embedding
is the right tool when the component is a part of your application rather than the application itself: a plugin, a sandboxed user script, a portable
business-rules engine, or a service that hosts many tenants’ components. Embedding gives full control over imports, limits, instance lifetimes and
pooling — PoolingAllocationConfig pre-reserves memory for thousands of instances and makes instantiation take microseconds, which is how serverless
platforms built on wasmtime achieve fast cold starts. The cost is code: a host is a few dozen lines for a simple case and grows with every capability
it exposes. Start with the CLI to validate the component, then embed once the shape of the host’s API is clear.
Expected output
wasmtime run tool.wasm -- input.txt behaves like the native tool within the preopened directories; wasmtime serve answers requests on port 8080; and the
Rust host prints hello-component-model from a typed call into the library component.
Gotchas
- “Component imports instance wasi:cli/…, but a matching implementation was not found.” WASI was not added to the linker. Call
add_to_linker. - No file access. Directories must be preopened with
--dir. Nothing is visible by default. - Component model not enabled. Older wasmtime versions require
wasm_component_model(true)in the config. - Version mismatch between
wasmtimeandwasmtime-wasi. Use the same version for both crates. - Sharing a Store across threads. Stores are not
Sync. Give each thread or task its own store. - Compiling per request. Compile once; instantiate per request.
Performance note
Compiling the 68 KB slugify component took 9 ms with Cranelift; loading a precompiled artefact took 0.3 ms. Instantiating took about 15 µs with the
default allocator and 4 µs with the pooling allocator, and a call to slugify took about 0.4 µs.
Frequently Asked Questions
Can wasmtime run components in other languages? Yes — any component, whether built from Rust, C, Go, Python or JavaScript, as long as its imports are provided.
Is wasmtime run safe for untrusted components?
The sandbox prevents access beyond granted capabilities; add memory and CPU limits for untrusted code.
Can I call a component from Python or Go? wasmtime’s Python and Go bindings support components in recent versions, with less mature typed bindings than Rust.
How do I debug a component?
Build it with debug info and run with -D debug-info, then attach a native debugger, or log through WASI stderr.
Can one Store hold several component instances? Yes, and they share the store’s limits and data. Use separate stores for isolation between tenants.
Related
- Composing two Wasm components — linking components before running.
- Using WIT resources and handles — host-implemented resources.
- Building HTTP services with Spin — a framework on top of wasmtime.
- Limiting plugin CPU and memory use — fuel, epochs and limiters.
← Back to Wasm Component Model & WIT Bindings