Build a Multi-Tenant Serverless Platform
Build a WebAssembly serverless runtime in Rust with tenant isolation.
Introduction
30 Second Summary
Every time you trigger a cloud function, there is a platform underneath deciding how to run your code safely alongside thousands of other users' code. That platform must prevent one tenant from crashing another, hogging all the CPU, or reading someone else's memory.
In this project, you will build a multi-tenant serverless platform prototype in Rust that uses WebAssembly sandboxes to isolate tenant code with CPU and memory limits enforced per invocation.
What You'll Build
WasmHost is a Rust HTTP server that accepts WebAssembly function deployments from multiple tenants and executes them in isolated sandboxes with fuel-based CPU metering and memory caps.
By the end of this project, you'll have:
- A REST API built with axum that exposes endpoints for registering tenants, deploying WASM functions, and invoking them.
- A Wasmtime-based execution engine with per-tenant isolation, fuel metering for CPU limits, and memory caps via StoreLimits.
- An extensible host function bridge that exposes services like logging to guest WASM modules, demonstrating how real platforms add capabilities to sandboxed code.
- Secret Mission: Add per-tenant rate limiting and a metrics dashboard endpoint that tracks invocation counts and fuel consumption.
Are there any prerequisites?
This project assumes intermediate Rust experience. You should be comfortable with ownership, traits, and async/await basics.
Familiarity with HTTP APIs (sending requests, reading JSON responses) is also expected.
Before We Start
Serverless platforms like AWS Lambda and Cloudflare Workers run millions of tenant functions side by side. The secret sauce is isolation. Each tenant's code must run in its own sandbox where it cannot access another tenant's memory, hog the CPU, or crash the host.
In this project, you will build a prototype serverless platform in Rust that uses WebAssembly sandboxing to isolate tenant functions. Before writing any platform code, you need the Rust toolchain, a WASM compilation target, and a project workspace.
In this step, get ready to:
- Install the Rust toolchain and verify it is version 1.92+ .
- Add the wasm32-wasip1 compilation target for building guest modules.
- Create the project workspace with cargo init.
Install the Rust toolchain
Rust is installed and managed through a tool called rustup. This project requires Rust 1.92 or higher because the Wasmtime runtime (our WebAssembly execution engine) has that as its minimum supported Rust version.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check whether Rust is already installed by running:
rustc --version
✔️ I see version 1.92 or higher
Your Rust toolchain is ready. Skip ahead to the next substep where you add the WASM target.
ⓧ I see an older version
You have Rust installed, but it is below 1.92. Update your toolchain by running:
rustup update
After the update completes, run rustc --version again to confirm it now shows 1.92 or higher.
Still seeing an old version?
Close your terminal and reopen it so the updated PATH takes effect. If the version is still too low, your system may have a separate rustc installed via a package manager that shadows the rustup version. Remove the system package (e.g. brew uninstall rust on macOS) and try again.
Help me fix my Rust version
ⓧ Command not found
Rust is not installed yet. Install it now.
macOS
- Install Rust by running this command:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
The installer will ask how you want to proceed. Press 1 then Enter to accept the default installation.
- After installation finishes, load the new PATH into your current terminal by running:
source "$HOME/.cargo/env"
- Verify the installation by running:
rustc --version
You should see a version number of 1.92 or higher.
Windows
- Download and run rustup-init.exe from https://rustup.rs.
- The installer will prompt you to install Visual Studio C++ Build Tools if they are not already present. Follow the on-screen instructions to install them.
- When asked how to proceed, type 1 and press Enter for the default installation.
- Close and reopen your terminal after installation completes.
- Verify the installation by running:
rustc --version
You should see a version number of 1.92 or higher.
Add the wasm32-wasip1 compilation target
Your Rust toolchain can compile code for many different platforms. To build WebAssembly guest modules, you need the wasm32-wasip1 target. This tells Cargo how to compile Rust code into WASM binaries that run inside a WASI-compatible runtime.
- Add the WASM compilation target by running:
rustup target add wasm32-wasip1
What is wasm32-wasip1?
WASI (WebAssembly System Interface) defines how WASM modules interact with the host operating system. The wasm32-wasip1 target compiles Rust code into 32-bit WebAssembly with WASI preview 1 support.
This is the stable, widely-supported version of WASI. Your guest functions will be compiled to this target so they can run inside the Wasmtime sandbox you build later.
- Confirm the target was added by running:
rustup target list --installed
You should see wasm32-wasip1 in the list of installed targets.
Don't see wasm32-wasip1 in the list?
Make sure the previous rustup target add command completed without errors. If you see a network error, check your internet connection and try again.
Help me add the wasm32-wasip1 target
Create the project workspace
Now you will create the Cargo project that will hold your serverless platform code.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project with Cargo by running:
cargo init wasmhost
What does cargo init do?
The cargo init command creates a new Rust project in the specified directory. It generates a Cargo.toml manifest (your project's configuration) and a src/main.rs file with a minimal "Hello, world!" program.
- Move into the new project directory by running:
cd wasmhost
- Confirm the project was created correctly by running:
ls src
You should see main.rs listed in the output.
- Verify the project compiles by running:
cargo build
You should see output ending with a "Finished" message. This confirms your toolchain, project structure, and build system are all working.
Seeing a compile error?
If cargo build fails with a linker error, you may need to install system build tools. On macOS, run xcode-select --install. On Ubuntu/Debian, run sudo apt install build-essential. On Windows, ensure Visual Studio Build Tools are installed (the rustup installer should have prompted you).
Help me fix my cargo build error
✔️ Awesome, I've got everything!
Great. Make sure you have saved your work and your terminal is inside the wasmhost directory.
ⓧ I'd like to double check the full code
Here is what your Cargo.toml should look like:
[package]
name = "wasmhost"
version = "0.1.0"
edition = "2024"
[dependencies]
And here is src/main.rs:
fn main() {
println!("Hello, world!");
}
Your development environment is ready. Next up, you will define the data model for tenants and their resource quotas.
Build the Tenant Registry and Data Model
A serverless platform handles code from many different users at the same time. Each user (or "tenant") gets their own namespace, resource limits, and deployed functions.
Before we can execute any WebAssembly code, we need a registry that tracks who each tenant is, what quota they have, and which function they have deployed. In this step, you will build the data model and in-memory store that the rest of the platform depends on.
In this step, get ready to:
- Define structs for resource quotas and tenant information.
- Build a thread-safe in-memory tenant store.
- Add methods to register, deploy, and look up tenants.
Define tenant, function metadata, and resource quota structs
The platform needs to know two things about each tenant: what resources they are allowed to consume, and what code they have deployed. We will model these as simple Rust structs with serialization support.
First, the project needs several dependencies. These include the HTTP framework, async runtime, WASM engine, and serialization libraries.
- Open Cargo.toml in your editor.
- Find the [dependencies] line (currently empty below it) and replace it with these dependencies:
[dependencies]
# HTTP framework for the REST API
axum = "0.8.9"
# Async runtime powering axum
tokio = { version = "1.52.3", features = ["full"] }
# WebAssembly runtime for executing tenant code
wasmtime = "44.0.1"
# Serialization/deserialization for JSON request bodies
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Ergonomic error handling
anyhow = "1"
What does each dependency do?
- axum is the HTTP framework that will power our REST API endpoints.
- tokio is the async runtime that axum runs on.
- wasmtime is the WebAssembly engine that sandboxes and executes tenant code.
- serde and serde_json handle converting Rust structs to and from JSON.
- anyhow provides ergonomic error propagation with the ? operator.
- Save Cargo.toml.
- Verify the dependencies resolve by running:
cargo check
- You should see Cargo download and compile the dependencies, finishing with a line that says Finished and no errors.
Seeing download or resolution errors?
- Check that your version strings match exactly. wasmtime = "44.0.1" needs the quotes and full version number.
- Make sure you have an internet connection. Cargo downloads crates from crates.io on first use.
- If you see a Rust version error, confirm your Rust version is 1.92+ with rustc --version.
Why won't my dependencies resolve?
Now create the tenant module file where the data model will live.
- Create a new file called tenant.rs inside the src/ folder of your wasmhost project.
- Add the imports and data model structs by pasting this code into src/tenant.rs:
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
// Defines how many resources a tenant is allowed to consume
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResourceQuota {
pub max_fuel: u64, // CPU instruction budget
pub max_memory_bytes: usize, // memory cap in bytes
}
// Holds all information about a single tenant
#[derive(Debug, Clone)]
pub struct TenantInfo {
pub id: String,
pub quota: ResourceQuota,
pub wasm_module: Option<Vec<u8>>, // deployed WASM bytecode (None if nothing deployed)
}
What do these structs represent?
ResourceQuota captures the two resource limits the platform enforces per tenant: CPU fuel (an instruction counter) and a memory ceiling in bytes.
TenantInfo is everything the platform knows about one tenant. The wasm_module field is Option<Vec<u8>> because a tenant can be registered before they deploy any code.
- Save src/tenant.rs.
- Confirm the file exists by running:
ls src/
- You should see both main.rs and tenant.rs in the output.
Implement an in-memory tenant store using Arc<RwLock<HashMap>>
The HTTP server will handle requests from multiple threads simultaneously. The tenant store needs to be safely shared across those threads while allowing concurrent reads and exclusive writes.
- In src/tenant.rs, add the TenantRegistry struct below the TenantInfo struct:
// Thread-safe store wrapping all tenant data
#[derive(Clone)]
pub struct TenantRegistry {
tenants: Arc<RwLock<HashMap<String, TenantInfo>>>,
}
Why Arc and RwLock?
Arc (atomic reference counting) lets multiple threads hold a pointer to the same data. RwLock allows many threads to read at the same time, but only one thread to write at a time.
Together they give us a thread-safe, concurrent HashMap. This is the same pattern production Rust services use for shared in-memory state.
- Save src/tenant.rs.
You won't see a visible change yet. The next chunk adds the methods that make this store useful.
Add methods to register tenants, deploy functions, and look up deployments
The registry needs three operations: creating a new tenant with a quota, storing a deployed WASM binary for a tenant, and retrieving tenant info for invocation.
- In src/tenant.rs, add the impl block below the TenantRegistry struct:
impl TenantRegistry {
// Creates an empty registry
pub fn new() -> Self {
Self {
tenants: Arc::new(RwLock::new(HashMap::new())),
}
}
// Registers a new tenant with their resource quota
pub fn register(&self, id: String, quota: ResourceQuota) {
let tenant = TenantInfo {
id: id.clone(),
quota,
wasm_module: None,
};
self.tenants.write().unwrap().insert(id, tenant);
}
// Stores WASM bytecode for an existing tenant
pub fn deploy(&self, tenant_id: &str, wasm_bytes: Vec<u8>) -> Result<(), String> {
let mut tenants = self.tenants.write().unwrap();
let tenant = tenants
.get_mut(tenant_id)
.ok_or_else(|| format!("tenant '{}' not found", tenant_id))?;
tenant.wasm_module = Some(wasm_bytes);
Ok(())
}
// Retrieves a clone of a tenant's info
pub fn get(&self, tenant_id: &str) -> Option<TenantInfo> {
self.tenants.read().unwrap().get(tenant_id).cloned()
}
}
What does each method do?
- new() creates an empty registry wrapped in the thread-safe layers.
- register() acquires a write lock and inserts a new tenant. The tenant starts with no deployed function.
- deploy() acquires a write lock, finds the tenant by ID, and stores the raw WASM bytes. It returns an error if the tenant does not exist.
- get() acquires a read lock and clones the tenant info. Cloning lets us release the lock immediately so other threads are not blocked.
- Save src/tenant.rs.
The file is not yet wired into the project's module tree, so cargo check will not compile it. That happens in the next step when we build the execution engine and connect the modules.
Seeing a syntax error in your editor?
- Check for a missing closing brace } on the impl block. Each method has its own braces, and the whole impl block needs a final one.
- Verify the deploy method's ok_or_else closure ends with a ? and a semicolon, not just the closure alone.
- Make sure the use statements at the top of the file import both Arc and RwLock from std::sync.
Help me fix a syntax error in my tenant.rs file.
✔️ Awesome, I've got everything!
Great. Double check you have saved both Cargo.toml and src/tenant.rs.
ⓧ I'd like to double check the full code
Here is the full Cargo.toml:
[package]
name = "wasmhost"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.9"
tokio = { version = "1.52.3", features = ["full"] }
wasmtime = "44.0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
Here is the full src/tenant.rs:
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResourceQuota {
pub max_fuel: u64,
pub max_memory_bytes: usize,
}
#[derive(Debug, Clone)]
pub struct TenantInfo {
pub id: String,
pub quota: ResourceQuota,
pub wasm_module: Option<Vec<u8>>,
}
#[derive(Clone)]
pub struct TenantRegistry {
tenants: Arc<RwLock<HashMap<String, TenantInfo>>>,
}
impl TenantRegistry {
pub fn new() -> Self {
Self {
tenants: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn register(&self, id: String, quota: ResourceQuota) {
let tenant = TenantInfo {
id: id.clone(),
quota,
wasm_module: None,
};
self.tenants.write().unwrap().insert(id, tenant);
}
pub fn deploy(&self, tenant_id: &str, wasm_bytes: Vec<u8>) -> Result<(), String> {
let mut tenants = self.tenants.write().unwrap();
let tenant = tenants
.get_mut(tenant_id)
.ok_or_else(|| format!("tenant '{}' not found", tenant_id))?;
tenant.wasm_module = Some(wasm_bytes);
Ok(())
}
pub fn get(&self, tenant_id: &str) -> Option<TenantInfo> {
self.tenants.read().unwrap().get(tenant_id).cloned()
}
}
Your data model is ready. Next up, you will build the WebAssembly execution engine that uses these quotas to sandbox tenant code.
Build the WebAssembly Execution Engine
In the previous step, you built a tenant registry that stores tenant info and their deployed WASM modules. Now you need something that actually runs those modules in a safe, isolated sandbox.
The execution engine uses Wasmtime to load each tenant's WebAssembly bytecode into its own sandboxed environment. Every invocation gets a fresh Store with fuel-based CPU metering and memory caps, preventing any tenant from monopolizing host resources.
In this step, get ready to:
- Configure a Wasmtime Engine with fuel-based CPU metering.
- Implement a run method with per-invocation Store isolation and memory limits.
- Wire up a host function bridge for guest-to-host communication.
Configure a Wasmtime Engine with fuel consumption enabled
The Engine is Wasmtime's compilation context. Enabling fuel consumption tells Wasmtime to count every instruction a guest module executes, giving you a kill switch for runaway code.
- Create a new file called src/engine.rs in your wasmhost project.
- Add the struct definitions, engine constructor, and host state helper:
use anyhow::{anyhow, Result};
use wasmtime::*;
pub struct ExecutionEngine {
engine: Engine,
}
pub struct ExecutionResult {
pub output: Vec<u8>,
pub fuel_consumed: u64,
}
impl ExecutionEngine {
pub fn new() -> Result<Self> {
// Enable fuel-based instruction counting
let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)?;
Ok(Self { engine })
}
}
// Holds the resource limiter for each Store
struct HostState {
limiter: StoreLimits,
}
impl HostState {
fn new() -> Self {
Self {
limiter: StoreLimitsBuilder::new()
.memory_size(256 * 1024 * 1024)
.build(),
}
}
}
What does this code do?
ExecutionEngine wraps a Wasmtime Engine configured with consume_fuel(true). This means every WebAssembly instruction costs fuel, and execution traps when fuel runs out.
ExecutionResult captures the output bytes and how much fuel was consumed, letting you report resource usage back to the caller.
HostState holds a StoreLimits built with StoreLimitsBuilder. The memory_size cap (256 MB) prevents any single invocation from allocating unbounded linear memory.
- Save src/engine.rs.
- Verify it compiles by running:
cargo build
You should see a warning about unused imports and dead code, but no errors.
Seeing compilation errors?
- Check that your Cargo.toml includes wasmtime = "44.0.1" and anyhow = "1" in [dependencies].
- Make sure you're using Rust 1.92+ since Wasmtime 44 requires it as the minimum supported Rust version.
- help me debug a compilation error in my engine.rs file
Implement the run method with isolated Store and fuel metering
Each invocation creates a fresh Store that holds the instance's runtime state. The Store is designed to be short-lived, so creating one per call gives you complete memory isolation between tenants.
- In src/engine.rs, find the impl ExecutionEngine block.
- Add the following run method after the new() method, inside the same impl block:
pub fn run(
&self,
wasm_bytes: &[u8],
input: &[u8],
max_fuel: u64,
max_memory_bytes: usize,
) -> Result<ExecutionResult> {
let module = Module::new(&self.engine, wasm_bytes)?;
// Create a fresh Store per invocation for isolation
let mut store = Store::new(&self.engine, HostState::new());
store.set_fuel(max_fuel)?;
store.limiter(|state| &mut state.limiter);
let mut linker = Linker::new(&self.engine);
// Expose a host_log function that guest WASM can call
linker.func_wrap(
"host",
"log",
|mut caller: Caller<'_, HostState>, ptr: i32, len: i32| {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
let data = memory.data(&caller);
let message =
std::str::from_utf8(&data[ptr as usize..(ptr as usize + len as usize)])
.unwrap_or("<invalid utf8>");
println!("[guest log] {}", message);
},
)?;
let instance = linker.instantiate(&mut store, &module)?;
// Get the guest's exported memory
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| anyhow!("guest did not export memory"))?;
// Allocate space in guest memory and copy input bytes
let alloc = instance.get_typed_func::<i32, i32>(&mut store, "alloc")?;
let input_ptr = alloc.call(&mut store, input.len() as i32)?;
memory.data_mut(&mut store)[input_ptr as usize..input_ptr as usize + input.len()]
.copy_from_slice(input);
// Call the guest's execute function with input pointer and length
let execute =
instance.get_typed_func::<(i32, i32), i32>(&mut store, "execute")?;
let output_len = execute.call(&mut store, (input_ptr, input.len() as i32))?;
// Retrieve the output from guest memory
let output_ptr_fn =
instance.get_typed_func::<(), i32>(&mut store, "get_output_ptr")?;
let output_ptr = output_ptr_fn.call(&mut store, ())?;
let output = memory.data(&store)
[output_ptr as usize..output_ptr as usize + output_len as usize]
.to_vec();
// Calculate fuel consumed for reporting
let fuel_remaining = store.get_fuel()?;
let fuel_consumed = max_fuel - fuel_remaining;
Ok(ExecutionResult {
output,
fuel_consumed,
})
}
What does this code do?
- Module::new compiles the raw WASM bytes into an executable module.
- Store::new creates an isolated execution context. Each Store is short-lived and independent, so one tenant's state never leaks into another's.
- set_fuel loads the CPU budget. Once fuel hits zero, Wasmtime traps the execution immediately.
- store.limiter connects the StoreLimits from HostState, enforcing the memory cap on linear memory growth.
- The Linker wires up host_log so guest modules can call log(ptr, len) to print messages through the host.
- The guest interaction sequence calls alloc (to reserve space), copies input bytes into guest memory, calls execute, then reads the output back via get_output_ptr.
- Save src/engine.rs.
- Verify the full file compiles by running:
cargo build
You should see warnings about unused code but zero errors.
Seeing a type mismatch or borrow error?
- Make sure the run method is inside the impl ExecutionEngine block (after new(), before the closing }).
- Verify you have a closing } for the impl ExecutionEngine block before the struct HostState definition.
- Check that the linker.func_wrap closure parameters match exactly: mut caller: Caller<'_, HostState>, ptr: i32, len: i32.
- help me fix a type or borrow error in my run method
Wire up a host function that guest modules can call
The host_log function you just added demonstrates the extensible service bridge pattern. Guest WASM modules import functions from the "host" namespace, and the Linker resolves those imports at instantiation time.
Why is the host function bridge important?
WebAssembly modules run in a pure sandbox with no access to the outside world by default. Host functions are the only way guest code can interact with the host system.
In production platforms, this pattern powers services like logging, key-value storage, HTTP clients, and secret managers. You define the interface, and the sandbox boundary stays intact.
How does this compare to real platforms?
AWS Lambda uses Firecracker microVMs for hardware-level isolation with ~125ms cold starts. Cloudflare Workers uses V8 isolates with sub-5ms cold starts. Your Wasmtime approach sits between them: stronger isolation than V8 isolates, lighter weight than full microVMs.
✔️ Awesome, I've got everything!
Great. Double check you've saved src/engine.rs and that cargo build completes without errors.
ⓧ I'd like to double check the full code
use anyhow::{anyhow, Result};
use wasmtime::*;
pub struct ExecutionEngine {
engine: Engine,
}
pub struct ExecutionResult {
pub output: Vec<u8>,
pub fuel_consumed: u64,
}
impl ExecutionEngine {
pub fn new() -> Result<Self> {
let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)?;
Ok(Self { engine })
}
pub fn run(
&self,
wasm_bytes: &[u8],
input: &[u8],
max_fuel: u64,
max_memory_bytes: usize,
) -> Result<ExecutionResult> {
let module = Module::new(&self.engine, wasm_bytes)?;
let mut store = Store::new(&self.engine, HostState::new());
store.set_fuel(max_fuel)?;
store.limiter(|state| &mut state.limiter);
let mut linker = Linker::new(&self.engine);
linker.func_wrap(
"host",
"log",
|mut caller: Caller<'_, HostState>, ptr: i32, len: i32| {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
let data = memory.data(&caller);
let message =
std::str::from_utf8(&data[ptr as usize..(ptr as usize + len as usize)])
.unwrap_or("<invalid utf8>");
println!("[guest log] {}", message);
},
)?;
let instance = linker.instantiate(&mut store, &module)?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| anyhow!("guest did not export memory"))?;
let alloc = instance.get_typed_func::<i32, i32>(&mut store, "alloc")?;
let input_ptr = alloc.call(&mut store, input.len() as i32)?;
memory.data_mut(&mut store)[input_ptr as usize..input_ptr as usize + input.len()]
.copy_from_slice(input);
let execute =
instance.get_typed_func::<(i32, i32), i32>(&mut store, "execute")?;
let output_len = execute.call(&mut store, (input_ptr, input.len() as i32))?;
let output_ptr_fn =
instance.get_typed_func::<(), i32>(&mut store, "get_output_ptr")?;
let output_ptr = output_ptr_fn.call(&mut store, ())?;
let output = memory.data(&store)
[output_ptr as usize..output_ptr as usize + output_len as usize]
.to_vec();
let fuel_remaining = store.get_fuel()?;
let fuel_consumed = max_fuel - fuel_remaining;
Ok(ExecutionResult {
output,
fuel_consumed,
})
}
}
struct HostState {
limiter: StoreLimits,
}
impl HostState {
fn new() -> Self {
Self {
limiter: StoreLimitsBuilder::new()
.memory_size(256 * 1024 * 1024)
.build(),
}
}
}
Your execution engine is ready to sandbox tenant code. Next up, you'll build the HTTP API that connects incoming requests to this engine so tenants can deploy and invoke their functions over the network.
Build the HTTP API
You have a tenant registry that tracks who is deployed and an execution engine that runs WASM in isolated sandboxes. The missing piece is a way for the outside world to interact with your platform.
In this step, you will build an HTTP API using axum that exposes endpoints for registering tenants, deploying functions, and invoking them. The API layer connects incoming requests to the registry and engine you already built.
In this step, get ready to:
- Create an axum Router with data types for requests and responses.
- Implement handler functions that connect to the tenant registry and execution engine.
- Wire up the main entry point to serve the API on port 3000.
Create the API module with data types and router
The API module defines the shape of every request and response, then maps URL paths to handler functions. You will create a new file src/api.rs that holds this entire HTTP layer.
- Create a new file called src/api.rs inside your wasmhost/src folder.
- Add the following imports, data types, and router function:
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::engine::ExecutionEngine;
use crate::tenant::{ResourceQuota, TenantRegistry};
use std::sync::Arc;
// Shared application state passed to every handler
pub struct AppState {
pub registry: TenantRegistry,
pub engine: Arc<ExecutionEngine>,
}
// JSON body for tenant registration requests
#[derive(Deserialize)]
pub struct RegisterRequest {
pub id: String,
pub max_fuel: u64,
pub max_memory_bytes: usize,
}
// Success response for registration and deploy
#[derive(Serialize)]
pub struct RegisterResponse {
pub message: String,
}
// Response returned after a successful function invocation
#[derive(Serialize)]
pub struct InvokeResponse {
pub output: String,
pub fuel_consumed: u64,
}
// Error response returned when something goes wrong
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
}
// Build the router with all routes and shared state
pub fn create_router(state: AppState) -> Router {
Router::new()
.route("/tenants", post(register_tenant))
.route("/tenants/{id}/deploy", post(deploy_function))
.route("/tenants/{id}/invoke", post(invoke_function))
.route("/health", get(health))
.with_state(Arc::new(state))
}
// Simple health check endpoint
async fn health() -> &'static str {
"ok"
}
What does this code do?
AppState bundles the tenant registry and execution engine so every handler can access them. The state is wrapped in Arc because axum clones the state for each request.
The four request/response structs define the JSON shapes that handlers produce and consume. create_router wires each URL path to a handler function.
Notice the path syntax uses {id} (curly braces). This is the axum 0.8 path parameter style.
Add the handler functions
Each handler extracts data from the incoming request, calls into the registry or engine, and returns a typed JSON response. Errors are handled by returning Result tuples with appropriate HTTP status codes.
- Below the health function in src/api.rs, add the register_tenant handler:
async fn register_tenant(
State(state): State<Arc<AppState>>,
Json(req): Json<RegisterRequest>,
) -> (StatusCode, Json<RegisterResponse>) {
let quota = ResourceQuota {
max_fuel: req.max_fuel,
max_memory_bytes: req.max_memory_bytes,
};
state.registry.register(req.id.clone(), quota);
(
StatusCode::CREATED,
Json(RegisterResponse {
message: format!("tenant '{}' registered", req.id),
}),
)
}
What does this handler do?
The State extractor pulls the shared AppState from the router. The Json extractor deserializes the request body into a RegisterRequest.
This handler always succeeds (no Result needed) because registering a tenant is infallible. It returns HTTP 201 Created with a confirmation message.
- Below register_tenant, add the deploy_function handler:
async fn deploy_function(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
body: axum::body::Bytes,
) -> Result<(StatusCode, Json<RegisterResponse>), (StatusCode, Json<ErrorResponse>)> {
state
.registry
.deploy(&id, body.to_vec())
.map(|_| {
(
StatusCode::OK,
Json(RegisterResponse {
message: format!("function deployed for tenant '{}'", id),
}),
)
})
.map_err(|e| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse { error: e }),
)
})
}
What does this handler do?
The Path(id) extractor captures the tenant ID from the URL. The raw Bytes extractor reads the request body as the compiled WASM binary.
If the tenant does not exist, deploy returns an Err which the handler maps to a 404 Not Found response. This is the first example of error handling through Result return types in axum.
- Below deploy_function, add the invoke_function handler:
async fn invoke_function(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
body: axum::body::Bytes,
) -> Result<(StatusCode, Json<InvokeResponse>), (StatusCode, Json<ErrorResponse>)> {
let tenant = state.registry.get(&id).ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("tenant '{}' not found", id),
}),
)
})?;
let wasm_bytes = tenant.wasm_module.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("no function deployed for tenant '{}'", id),
}),
)
})?;
let result = state
.engine
.run(
&wasm_bytes,
&body,
tenant.quota.max_fuel,
tenant.quota.max_memory_bytes,
)
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("execution failed: {}", e),
}),
)
})?;
Ok((
StatusCode::OK,
Json(InvokeResponse {
output: String::from_utf8_lossy(&result.output).to_string(),
fuel_consumed: result.fuel_consumed,
}),
))
}
What does this handler do?
This is the most complex handler because it chains three fallible operations. First it looks up the tenant. Then it checks that a WASM module has been deployed. Finally it runs the module through the execution engine.
Each failure point maps to a different HTTP status: 404 for missing tenant or module, 500 for execution traps (like fuel exhaustion). The ? operator propagates errors early, keeping the happy path clean.
- Save src/api.rs.
Seeing red squiggles in your editor?
Your editor might show errors about unresolved imports like crate::engine or crate::tenant. This is expected because main.rs has not declared the modules yet. You will fix this in the next substep.
help me understand why my Rust module imports are not resolving
✔️ Awesome, I've got everything!
Great. Double check you have saved src/api.rs.
ⓧ I'd like to double check the full code
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use crate::engine::ExecutionEngine;
use crate::tenant::{ResourceQuota, TenantRegistry};
use std::sync::Arc;
pub struct AppState {
pub registry: TenantRegistry,
pub engine: Arc<ExecutionEngine>,
}
#[derive(Deserialize)]
pub struct RegisterRequest {
pub id: String,
pub max_fuel: u64,
pub max_memory_bytes: usize,
}
#[derive(Serialize)]
pub struct RegisterResponse {
pub message: String,
}
#[derive(Serialize)]
pub struct InvokeResponse {
pub output: String,
pub fuel_consumed: u64,
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
}
pub fn create_router(state: AppState) -> Router {
Router::new()
.route("/tenants", post(register_tenant))
.route("/tenants/{id}/deploy", post(deploy_function))
.route("/tenants/{id}/invoke", post(invoke_function))
.route("/health", get(health))
.with_state(Arc::new(state))
}
async fn health() -> &'static str {
"ok"
}
async fn register_tenant(
State(state): State<Arc<AppState>>,
Json(req): Json<RegisterRequest>,
) -> (StatusCode, Json<RegisterResponse>) {
let quota = ResourceQuota {
max_fuel: req.max_fuel,
max_memory_bytes: req.max_memory_bytes,
};
state.registry.register(req.id.clone(), quota);
(
StatusCode::CREATED,
Json(RegisterResponse {
message: format!("tenant '{}' registered", req.id),
}),
)
}
async fn deploy_function(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
body: axum::body::Bytes,
) -> Result<(StatusCode, Json<RegisterResponse>), (StatusCode, Json<ErrorResponse>)> {
state
.registry
.deploy(&id, body.to_vec())
.map(|_| {
(
StatusCode::OK,
Json(RegisterResponse {
message: format!("function deployed for tenant '{}'", id),
}),
)
})
.map_err(|e| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse { error: e }),
)
})
}
async fn invoke_function(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
body: axum::body::Bytes,
) -> Result<(StatusCode, Json<InvokeResponse>), (StatusCode, Json<ErrorResponse>)> {
let tenant = state.registry.get(&id).ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("tenant '{}' not found", id),
}),
)
})?;
let wasm_bytes = tenant.wasm_module.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("no function deployed for tenant '{}'", id),
}),
)
})?;
let result = state
.engine
.run(
&wasm_bytes,
&body,
tenant.quota.max_fuel,
tenant.quota.max_memory_bytes,
)
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("execution failed: {}", e),
}),
)
})?;
Ok((
StatusCode::OK,
Json(InvokeResponse {
output: String::from_utf8_lossy(&result.output).to_string(),
fuel_consumed: result.fuel_consumed,
}),
))
}
Update the main entry point
The final piece is main.rs. It needs to declare all three modules, instantiate the shared state, and start the HTTP server.
- Open src/main.rs and replace its entire contents with the following:
mod api;
mod engine;
mod tenant;
use api::{create_router, AppState};
use engine::ExecutionEngine;
use tenant::TenantRegistry;
use std::sync::Arc;
#[tokio::main]
async fn main() {
println!("Starting WasmHost serverless platform...");
// Create the shared tenant registry and execution engine
let registry = TenantRegistry::new();
let engine = Arc::new(ExecutionEngine::new().expect("failed to create execution engine"));
// Bundle into application state and build the router
let state = AppState { registry, engine };
let app = create_router(state);
// Bind to port 3000 and start serving
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("failed to bind to port 3000");
println!("WasmHost listening on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}
What does this code do?
The three mod declarations tell the Rust compiler to include tenant.rs, engine.rs, and api.rs as part of the crate. Without these, the crate:: imports in api.rs would not resolve.
The #[tokio::main] macro transforms main into an async entry point. axum::serve takes a TCP listener and the router, then runs the server loop until the process exits.
- Save src/main.rs.
- Verify the project compiles by running:
cargo build
- You should see output ending with a line like Finished and no errors.
Seeing compilation errors?
If you see an error about unresolved imports, make sure you have all three mod lines at the top of main.rs. The module names must match the filenames exactly (api, engine, tenant).
If you see a type mismatch related to State, check that create_router wraps state in Arc::new(state) and all handlers extract State<Arc<AppState>>.
help me fix a compilation error in my axum API module
✔️ Awesome, I've got everything!
Great. Double check you have saved src/main.rs.
ⓧ I'd like to double check the full code
mod api;
mod engine;
mod tenant;
use api::{create_router, AppState};
use engine::ExecutionEngine;
use tenant::TenantRegistry;
use std::sync::Arc;
#[tokio::main]
async fn main() {
println!("Starting WasmHost serverless platform...");
let registry = TenantRegistry::new();
let engine = Arc::new(ExecutionEngine::new().expect("failed to create execution engine"));
let state = AppState { registry, engine };
let app = create_router(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("failed to bind to port 3000");
println!("WasmHost listening on http://0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}
Your HTTP API is wired up and the project compiles cleanly. Next, you will build a sample guest function in WASM and test the full deploy-and-invoke cycle end to end.
Build and Deploy a Sample Guest Function
Your platform runtime is complete. It can register tenants, accept deployments, and execute WebAssembly modules with resource limits enforced.
Now you need something to run on it. In this step, you will write a minimal guest function in Rust, compile it to wasm32-wasip1, and test the entire deploy-and-invoke cycle end to end using curl.
In this step, get ready to:
- Write a Rust guest module that exports memory management and processing functions.
- Compile the guest to a WASM binary and invoke it through the WasmHost API.
Write a minimal Rust guest module
A guest module is the code that tenants deploy to your platform. It runs inside the Wasmtime sandbox with no access to the host filesystem, network, or memory outside its own linear memory.
The guest must export specific functions that the execution engine calls: alloc for the host to write input into guest memory, execute to process the input, and get_output_ptr so the host can read the result back out.
- Navigate up from your wasmhost directory and create the guest crate by running these commands:
cd ..
cargo new guest --lib
- Confirm the files exist by running:
ls guest/src
You should see lib.rs listed in the output.
Why a separate crate?
The guest compiles to a completely different target (wasm32-wasip1) than your host (your native OS). Keeping it in a separate crate with its own Cargo.toml means each crate can have its own target and dependencies without conflicts.
- Open guest/Cargo.toml in your editor and replace its contents with the following:
[package]
name = "guest"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
What does this configuration do?
The [lib] section tells Cargo to compile this crate as a C-compatible dynamic library (cdylib). For the wasm32-wasip1 target, this produces a standalone .wasm file that Wasmtime can load directly.
- Save guest/Cargo.toml.
- Open guest/src/lib.rs and replace any existing content with the memory interface code below:
use std::alloc::{alloc, Layout};
extern "C" {
fn log(ptr: i32, len: i32);
}
static mut OUTPUT_BUF: Vec<u8> = Vec::new();
#[no_mangle]
pub extern "C" fn alloc(size: i32) -> i32 {
let layout = Layout::from_size_align(size as usize, 1).unwrap();
unsafe { alloc(layout) as i32 }
}
What does this code do?
The extern "C" block declares the log function that the host provides. The guest can call it to send messages back to the host's console.
The OUTPUT_BUF static stores the guest's output so the host can read it after execution finishes. The alloc function lets the host allocate space in guest memory to write input data into.
- Add the processing logic below the alloc function in the same file:
#[no_mangle]
pub extern "C" fn execute(input_ptr: i32, input_len: i32) -> i32 {
let input =
unsafe { std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize) };
let message = b"Processing input";
unsafe {
log(message.as_ptr() as i32, message.len() as i32);
}
let mut output = b"Hello from guest! You sent: ".to_vec();
output.extend_from_slice(input);
let len = output.len();
unsafe {
OUTPUT_BUF = output;
}
len as i32
}
#[no_mangle]
pub extern "C" fn get_output_ptr() -> i32 {
unsafe { OUTPUT_BUF.as_ptr() as i32 }
}
What does this code do?
The execute function is the guest's entry point. It reads the input bytes the host wrote into memory, calls the host's log function, then builds an output string and stores it in OUTPUT_BUF. It returns the output length so the host knows how many bytes to read.
The get_output_ptr function returns the memory address where the output starts. The host calls this after execute to locate and copy the result out of guest memory.
- Save guest/src/lib.rs.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both guest/Cargo.toml and guest/src/lib.rs.
ⓧ I'd like to double check the full code
Here is the complete guest/Cargo.toml:
[package]
name = "guest"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
Here is the complete guest/src/lib.rs:
use std::alloc::{alloc, Layout};
extern "C" {
fn log(ptr: i32, len: i32);
}
static mut OUTPUT_BUF: Vec<u8> = Vec::new();
#[no_mangle]
pub extern "C" fn alloc(size: i32) -> i32 {
let layout = Layout::from_size_align(size as usize, 1).unwrap();
unsafe { alloc(layout) as i32 }
}
#[no_mangle]
pub extern "C" fn execute(input_ptr: i32, input_len: i32) -> i32 {
let input =
unsafe { std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize) };
let message = b"Processing input";
unsafe {
log(message.as_ptr() as i32, message.len() as i32);
}
let mut output = b"Hello from guest! You sent: ".to_vec();
output.extend_from_slice(input);
let len = output.len();
unsafe {
OUTPUT_BUF = output;
}
len as i32
}
#[no_mangle]
pub extern "C" fn get_output_ptr() -> i32 {
unsafe { OUTPUT_BUF.as_ptr() as i32 }
}
Compile to wasm32-wasip1 and test the full cycle
With the guest code written, you can compile it to a .wasm binary and run through the complete serverless workflow: register a tenant, deploy the binary, and invoke the function.
- Compile the guest module in release mode by running:
cd guest
cargo build --target wasm32-wasip1 --release
You should see output ending with Compiling guest v0.1.0 and Finished with no errors.
Seeing a compilation error?
If you see an error about the target not being installed, make sure you added the wasm32-wasip1 target in Step 1. You can add it now by running:
rustup target add wasm32-wasip1
Then retry the build command.
Help me fix my guest compilation error.
- Confirm the WASM binary was produced by running:
ls target/wasm32-wasip1/release/guest.wasm
You should see the file path echoed back. This is the compiled module you will deploy to WasmHost.
- Navigate to the wasmhost directory and start the server by running:
cd ../wasmhost
cargo run
You should see WasmHost listening on http://0.0.0.0:3000 printed to the terminal. The server is now blocking this terminal.
- Open a second terminal window. Press Cmd+Space (macOS) or the Windows key (Windows), type Terminal, and press Enter.
- Navigate to the parent directory containing both projects by running:
cd ~/Desktop
- Register a test tenant by running this curl command:
curl -X POST http://localhost:3000/tenants \
-H "Content-Type: application/json" \
-d '{"id": "tenant1", "max_fuel": 1000000, "max_memory_bytes": 10485760}'
You should see a JSON response: {"message":"tenant 'tenant1' registered"}.
- Deploy the compiled WASM binary to the tenant by running:
curl -X POST http://localhost:3000/tenants/tenant1/deploy \
--data-binary @guest/target/wasm32-wasip1/release/guest.wasm
You should see: {"message":"function deployed for tenant 'tenant1'"}.
- Invoke the deployed function with some input by running:
curl -X POST http://localhost:3000/tenants/tenant1/invoke \
-d 'hello world'
You should see a JSON response containing "output":"Hello from guest! You sent: hello world" along with a fuel_consumed value showing how many fuel units the execution cost.
What just happened?
You ran through the complete serverless lifecycle. The platform received raw WASM bytes, stored them for the tenant, then on invocation: compiled the module, created an isolated Store with fuel and memory limits, wrote input into guest memory via the exported alloc function, called execute, and read the output back.
If you check the first terminal running the server, you should also see [guest log] Processing input printed. That message came from inside the sandbox, routed through the host function bridge.
Getting a 'tenant not found' or 'no function deployed' error?
Make sure you ran the register command before deploy, and deploy before invoke. Each curl command depends on the previous one succeeding.
If the deploy command returns a 404, double-check the tenant ID matches exactly between the register and deploy URLs (tenant1 in both).
Help me debug my curl invocation errors.
Your guest function is live and responding to invocations. Next up, you will register multiple tenants and prove that resource isolation prevents one tenant from affecting another.
Test Multi-Tenancy and Resource Isolation
Your serverless platform can deploy and invoke functions for a single tenant. Now it's time to answer the critical question: can one tenant's misbehavior crash another tenant's execution?
In this step, you'll register two tenants with deliberately different fuel budgets. You'll prove that when one tenant exhausts their CPU quota, the other tenant continues to run without interruption.
In this step, get ready to:
- Register two tenants with different resource quotas.
- Deploy the guest function to both tenants and invoke them independently.
- Verify that fuel exhaustion produces a clean error and does not affect other tenants.
Register two tenants with different resource quotas
To test isolation, you need two tenants with intentionally different fuel budgets. One tenant gets a generous allowance (1,000,000 fuel units). The other gets a tiny budget (1,000 fuel units) that will run out before the guest function finishes.
- Register tenant-a with a high fuel budget by running this command:
curl -X POST http://localhost:3000/tenants \
-H "Content-Type: application/json" \
-d '{"id": "tenant-a", "max_fuel": 1000000, "max_memory_bytes": 10485760}'
You should see a response confirming the tenant was registered:
{"message":"tenant 'tenant-a' registered"}
- Register tenant-b with a deliberately low fuel budget by running this command:
curl -X POST http://localhost:3000/tenants \
-H "Content-Type: application/json" \
-d '{"id": "tenant-b", "max_fuel": 1000, "max_memory_bytes": 10485760}'
Why 1,000 fuel for tenant-b?
Wasmtime fuel counts individual CPU instructions. Even a simple function like our guest needs thousands of fuel units to allocate memory, copy bytes, and build its output.
Setting max_fuel to 1,000 guarantees the function will run out of fuel mid-execution. This simulates a tenant trying to run a computation that exceeds their quota.
You should see the same registration confirmation for tenant-b.
Deploy the guest function to both tenants
Both tenants will run the same guest function. The difference is purely in their resource quotas. This mirrors real platforms where many tenants run similar workloads but pay for different tiers.
- Deploy guest.wasm to tenant-a by running this command:
curl -X POST http://localhost:3000/tenants/tenant-a/deploy \
--data-binary @../guest/target/wasm32-wasip1/release/guest.wasm
- Deploy guest.wasm to tenant-b by running this command:
curl -X POST http://localhost:3000/tenants/tenant-b/deploy \
--data-binary @../guest/target/wasm32-wasip1/release/guest.wasm
Both commands should return a success message confirming the function was deployed.
- Invoke tenant-a to confirm it works by running this command:
curl -X POST http://localhost:3000/tenants/tenant-a/invoke \
-d 'hello from tenant-a'
You should see a successful response with the guest's output and the fuel consumed:
{"output":"Hello from guest! You sent: hello from tenant-a","fuel_consumed":...}
The fuel_consumed value tells you exactly how many instruction units the guest function used. Note this number for the next substep.
Getting a 'tenant not found' error?
Make sure the server is still running in your other terminal. The in-memory registry resets every time you restart the server, so you would need to re-register tenants if you restarted it.
Help me debug my tenant invocation
Verify fuel exhaustion and tenant isolation
This is the moment of truth. Tenant-b has only 1,000 fuel units, which is not enough to complete the guest function. If your isolation works correctly, tenant-b's failure will not affect tenant-a at all.
- Invoke tenant-b to trigger fuel exhaustion by running this command:
curl -X POST http://localhost:3000/tenants/tenant-b/invoke \
-d 'hello from tenant-b'
You should see an HTTP 500 response with a JSON error body. The error message starts with execution failed: followed by a fuel-related trap message from Wasmtime.
What just happened?
The guest function tried to execute more instructions than its 1,000 fuel budget allowed. Wasmtime detected the overrun and trapped the execution immediately. The platform caught the trap, converted it into a clean HTTP error response, and stayed running.
This is exactly how production platforms like AWS Lambda handle timeouts. The tenant's function is terminated, the platform reports the failure, and all other tenants are unaffected.
- Now invoke tenant-a again to prove isolation by running this command:
curl -X POST http://localhost:3000/tenants/tenant-a/invoke \
-d 'still working after tenant-b crashed'
You should see a successful response with the output Hello from guest! You sent: still working after tenant-b crashed and a fuel_consumed value. Tenant-a is completely unaffected by tenant-b's crash.
Why does this work?
Each invocation creates a fresh Store in Wasmtime. A Store is the execution context that holds fuel counters, memory, and instance state. Because tenant-a and tenant-b never share a Store, one tenant's fuel exhaustion cannot corrupt or halt another tenant's execution.
The shared Engine (compilation context) is safe to share because compiled modules are read-only. All mutable state lives in the per-invocation Store.
Tenant-b succeeded instead of failing?
If tenant-b returned a successful response, the fuel value might be too high. Check that you registered tenant-b with max_fuel set to 1000 (not 1,000,000). If you made a typo, register a new tenant with the correct value and try again.
Help me debug why my fuel limit isn't working
Secret mission
Add Per-Tenant Rate Limiting and Metrics
Your serverless platform enforces CPU and memory limits, but what about request volume? A single tenant could flood the system with invocations, starving others of resources. In this mission, you'll add a sliding-window rate limiter and an execution metrics endpoint to give each tenant observability into their own usage.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The WasmHost server process (running in your terminal).
- The wasmhost project folder on your Desktop.
- The guest project folder on your Desktop.
- The wasm32-wasip1 Rust compilation target.
- The Rust toolchain (installed via rustup).
Keep everything running
No action needed. Choose this if you're still actively building or plan to extend the platform further.
- Your WasmHost server stays running until you close the terminal or press Ctrl+C.
- The Rust toolchain and wasm32-wasip1 target remain available for future Rust and WebAssembly projects.
- All project files stay on your Desktop.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your project files remain on disk for when you return.
- In the terminal where WasmHost is running, press Ctrl+C to stop the server.
- Your wasmhost and guest folders stay on your Desktop.
- To restart later, open a terminal, navigate to the wasmhost folder, and run cargo run.
Delete - I don't want to use this again
Remove all project files and optionally uninstall the Rust toolchain entirely.
- In the terminal where WasmHost is running, press Ctrl+C to stop the server.
Delete the project folders by running these commands:
cd ~/Desktop
rm -rf wasmhost
rm -rf guest
On Windows, use these commands instead:
cd ~\Desktop
rmdir /s /q wasmhost
rmdir /s /q guest
Remove the WebAssembly compilation target:
rustup target remove wasm32-wasip1
If you no longer need Rust on your machine, uninstall the entire toolchain:
rustup self uninstall
- This removes rustup, all installed toolchains, and all associated data.
Nice Work!
Nice Work!
Nice work! You just built a multi-tenant serverless platform prototype in Rust that executes WebAssembly functions in fully isolated sandboxes.
You've learned how to:
- Build a multi-tenant WebAssembly runtime using Wasmtime, with per-tenant isolation via fresh Stores and resource metering via fuel-based CPU limits and StoreLimits memory caps.
- Create an extensible host function bridge that exposes services to guest WASM modules, and deploy and invoke those modules through a REST API built with axum.
- Prove that tenant isolation prevents resource interference by exhausting one tenant's fuel quota without affecting another tenant's execution.
- Added per-tenant rate limiting with a sliding-window algorithm and an observability metrics endpoint that tracks invocation count and fuel consumption.
Ready to quiz yourself?