A Beginner's Guide to WASI (WebAssembly System Interface)
WasmHub Team
March 12, 2025 · 6 min read
WebAssembly's original design was deliberately minimal. A Wasm module computes, but it can't read a file, open a socket, print to the terminal, or even tell the time — not without the host environment providing those capabilities explicitly. In a browser, the host is JavaScript. But what about running Wasm on a server, in a CLI tool, or inside a database? That's the problem WASI was built to solve.
What Is WASI?
The WebAssembly System Interface is a standardized set of APIs that define how a Wasm module interacts with the outside world. Think of it as POSIX for WebAssembly — but designed from scratch with security and portability as the primary constraints, not an afterthought.
WASI is maintained by the Bytecode Alliance and implemented by all major runtimes: Wasmtime, Wasmer, WasmEdge, and the WASI runtimes in Deno, Node.js, and Bun. A WASI-compliant binary compiled on your laptop runs unmodified on any of those runtimes, on any architecture, without recompilation.
The Capability Security Model
The most important thing about WASI isn't the list of APIs — it's the security model. WASI uses capabilities: a module can only access a resource if the host explicitly hands it a handle to that resource. No ambient authority, no implicit filesystem access, no sneaking a socket past the sandbox.
In practice this looks like this: when you run a WASI binary, you grant it access to specific directories:
# The module can only see /tmp — nothing else on the filesystem
wasmtime run --dir /tmp my-module.wasm
# Grant two directories
wasmtime run --dir /data --dir /config my-module.wasm
# Map a host path to a different path inside the module
wasmtime run --dir /var/myapp/data::/data my-module.wasmA module that tries to open("/etc/passwd") without being granted /etc gets a permission error. No firewall rules, no seccomp filters, no container layers — the sandbox is enforced at the API boundary.
Writing Your First WASI Module in Rust
The fastest path to a working WASI module is Rust with the wasm32-wasip1 or wasm32-wasip2 target. Add the target and write a simple program:
rustup target add wasm32-wasip1
cargo new wasi-hello && cd wasi-hello// src/main.rs
use std::fs;
use std::io::{self, Write};
fn main() {
// These syscalls go through WASI — not the host OS directly
let args: Vec<String> = std::env::args().collect();
let name = args.get(1).map(String::as_str).unwrap_or("world");
// Write to stdout via WASI fd_write
println!("Hello, {}!", name);
// Read a file via WASI path_open + fd_read
if let Ok(contents) = fs::read_to_string("greeting.txt") {
print!("File says: {}", contents);
}
// Write a file via WASI path_open (with write flag) + fd_write
let mut output = fs::File::create("output.txt")
.expect("need write access to current dir");
writeln!(output, "Written by WASI at runtime").unwrap();
}cargo build --target wasm32-wasip1 --release
# Run it — grant access to the current directory
wasmtime run \
--dir . \
target/wasm32-wasip1/release/wasi-hello.wasm \
-- WasmHubEvery println!, File::open, and File::create call compiles to a WASI host function import — fd_write, path_open, fd_read. The Rust standard library handles the translation; you write idiomatic Rust and WASI handles the rest.
WASI Preview 1 vs Preview 2
WASI Preview 1 (the wasi_snapshot_preview1 ABI) is the stable, widely-supported version you'll find in most runtimes and language targets today. It covers the basics: filesystems, clocks, random number generation, environment variables, and process exit.
WASI Preview 2 (stabilized in late 2024) is a complete redesign. Instead of a flat list of C-style function imports, it's built entirely on the Component Model and described with WIT (WebAssembly Interface Types). The same capabilities become typed interfaces:
// WASI Preview 2 filesystem interface (simplified)
package wasi:filesystem@0.2.0;
interface types {
resource descriptor {
read: func(length: filesize) -> result<tuple<list<u8>, bool>, error-code>;
write: func(buffer: list<u8>) -> result<filesize, error-code>;
stat: func() -> result<descriptor-stat, error-code>;
}
}The practical upshot: Preview 2 supports richer types (strings, records, variants), enables composition between WASI modules, and lays the groundwork for the networking and async APIs coming in WASI 0.3. The wasm32-wasip2 target is stable in Rust as of version 1.78.
What WASI Covers Today
| Interface | Preview 1 | Preview 2 |
|---|---|---|
| Filesystem (read/write) | ✅ | ✅ (typed) |
| Clocks & time | ✅ | ✅ |
| Random numbers | ✅ | ✅ |
| Environment variables | ✅ | ✅ |
| TCP/UDP sockets | ❌ | ✅ (wasi:sockets) |
| HTTP client | ❌ | ✅ (wasi:http) |
| Threads | ❌ | 🚧 (in progress) |
| Async I/O | ❌ | 🚧 (WASI 0.3) |
The addition of wasi:sockets and wasi:http in Preview 2 is significant: modules can now open TCP connections and make HTTP requests without any JavaScript glue — the runtime provides the capability handle and the module uses it directly.
Running WASI Modules in JavaScript Environments
WASI isn't just for CLI tools and servers. Node.js 22+ and Deno 1.40+ include built-in WASI support:
// Node.js 22+ — built-in WASI module
import { WASI } from 'wasi'
import { readFile } from 'fs/promises'
const wasi = new WASI({
version: 'preview1',
args: ['my-module', '--verbose'],
env: { LOG_LEVEL: 'info' },
preopens: {
'/data': '/var/myapp/data', // virtual path : real path
},
})
const wasm = await WebAssembly.compile(
await readFile('./my-module.wasm')
)
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject())
wasi.start(instance)The preopens map is the capability grant — it's the Node.js equivalent of --dir in wasmtime run. The module sees /data; the host controls what /data maps to.
Where WASI Is Headed
WASI 0.3 is the next major milestone. Its headline feature is native async support using Wasm's stack-switching proposal: modules can await I/O operations without blocking a thread, using the same mental model as async Rust or JavaScript. This will make WASI genuinely practical for network services that handle many concurrent connections.
Beyond that, the working groups are specifying interfaces for:
wasi:messaging— Kafka-style message queueswasi:keyvalue— KV store access (Redis, DynamoDB, etc.)wasi:sql— Relational database querieswasi:gpu— Compute shader access via WebGPU
Each of these follows the same pattern: a WIT interface that any runtime can implement, so your module stays portable while the host provides the real backend.
The Big Picture
WASI turns WebAssembly into a universal application platform. Write your code once, compile to Wasm, and run it on any WASI-compliant runtime — Wasmtime on Linux, WasmEdge in a Kubernetes pod, Fermyon Spin on a serverless platform, or the built-in WASI in Deno. The capability model gives platform operators fine-grained control over what each module can access, without managing containers or writing custom security policies.
If you've been thinking of Wasm as a browser technology, WASI is the piece that makes it a general-purpose computing substrate. It's worth learning now.