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 wasmtime and wasmtime-wasi crates, 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.

How to run a component, by its world A command component exporting wasi:cli/run runs with wasmtime run. An HTTP component exporting wasi:http/incoming-handler runs with wasmtime serve. A library component exporting a custom interface is embedded in a Rust host and called through bindgen-generated types. Which world does the component export? wasi:cli/run wasmtime run app.wasm --arg … wasi:http/incoming-handler wasmtime serve handler.wasm custom interface embed with the Rust API and bindgen!

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.

The pieces of a wasmtime component host The Engine holds configuration and compiled code. A Component is compiled once per engine. The Linker supplies imports such as WASI. A Store holds per-instance state such as the WASI context. bindgen generates typed wrappers so the host calls exports with Rust types. bindgen! wrappers Slugger::instantiate, call_slugify(&str, &Options) Store<Host> per-instance state: WASI ctx, resource table Linker<Host> imports: wasmtime_wasi + custom host functions Component compiled once, instantiated many times Engine configuration + code cache

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 wasmtime and wasmtime-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.

Getting a component ready to call Time to obtain a callable instance of a small component in wasmtime by compiling from the .wasm file, deserialising a precompiled artefact, and instantiating from an already loaded component with the pooling allocator. ms until callable compile from .wasm 9 ms deserialize precompiled 0.3 ms instantiate (pooling) 0.0 ms

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.

← Back to Wasm Component Model & WIT Bindings