What WebAssembly actually is: a portable instruction set, not a language
The name misleads almost everyone at first. WebAssembly — usually shortened to Wasm — is not a language you write, and despite the "Web" it is no longer tied to browsers. It is a compact, low-level binary instruction format: a virtual instruction set designed to be the compilation target for languages like C, C++, and Rust. You compile your source down to a `.wasm` file of bytecode, much as you would compile to x86-64; the difference is that this instruction set was designed from scratch to be portable, compact, and safe to run code you do not trust. Think of it as a new kind of executable whose CPU is software.
What does the instruction set look like? Wasm is a stack machine, which should feel familiar after the assembly rung. Instead of named registers like rax and rsp, instructions push and pop operands on an implicit operand stack. `i32.const 7` pushes the 32-bit integer 7; a following `i32.const 5` pushes 5; then `i32.add` pops both, adds them, and pushes 12. The types are deliberately spare — `i32`, `i64`, `f32`, `f64` (32- and 64-bit integers and floats), plus a few reference types. There are no `unsigned long` or `struct` instructions; a higher-level type is just a pattern of these primitives laid out in memory, exactly the bytes-versus-interpretation view you already hold.
WAT (the text form of Wasm) for add(a, b):
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a ;; push a
local.get $b ;; push b
i32.add) ;; pop both, push a+b
source (C): int add(int a, int b) { return a + b; }
compiled .wasm bytes: 20 00 20 01 6a (local.get/local.get/i32.add)The sandbox: linear memory and why it cannot escape
Here is the idea that makes Wasm a frontier rather than just another bytecode. A Wasm module's entire mutable memory is a single contiguous array of bytes called its linear memory — literally one flat byte array, the picture from the pointers rung made concrete. A pointer inside a Wasm module is not a host machine address; it is an index into that array, starting at 0. When the module loads a value at "address" 0x1F, the runtime computes it as `base + 0x1F`, where `base` is where the host happened to put the array, and crucially it checks that the index is within bounds before touching anything.
Trace what that buys you. In native code, a buffer overflow that writes past the end of an array runs straight into whatever the operating system put next — another object, a return address, allocator metadata — and from the security rung you know that is the seed of an exploit. Inside Wasm, the same out-of-bounds write cannot reach anything outside the linear memory array, because every access is bounds-checked and the array is all there is. The worst a buggy or malicious module can do is corrupt its own sandbox. It has no view of the host's address space, no way to forge a pointer to the host, and no instructions that name a host address at all. The unsafe code is still unsafe — but it is unsafe only to itself.
The runtime: how a .wasm file actually runs
A `.wasm` file is data, not a native executable; something has to run it. That something is a Wasm runtime — a program on the host (wasmtime, Wasmer, WAMR, V8, and others) that loads the module and turns its bytecode into real CPU instructions. Before running a single instruction it does validation: a fast pass that statically checks the bytecode is well-typed and structured — every stack operation has the right operand types, every jump lands in-bounds, no function returns a value it never produced. This is closer to ahead-of-time verification than the undefined behavior of C: a validated module cannot be in the malformed states that make native bugs unbounded. Code that fails validation never runs at all.
After validation the runtime executes the module one of two ways. A simple runtime interprets the bytecode, like the interpreter idea from the foundations rung — easy, portable, but slower. A serious runtime instead JIT-compiles it: a just-in-time compiler translates each Wasm function into native x86-64 or ARM instructions the first time it is called, so the loop you wrote runs as real machine code, not as bytecode walked one step at a time. Because Wasm's instructions map closely onto hardware ones and the types are concrete, this codegen is fast and the result runs at near-native speed — typically within a small percentage of the same C compiled straight to the host. Portable-but-slow and native-but-unsafe used to be a forced choice; this is the frontier that loosens it.
WASI: giving the sandbox a way to touch the world
A perfectly sealed sandbox is also perfectly useless: pure Wasm has no instruction to open a file, read a socket, or get the time, because none of those live inside the linear memory array. Every real effect must cross the boundary by calling out to a function the host provides — an import. This is where WASI, the WebAssembly System Interface, comes in. WASI is a standardized set of imported functions that look much like the POSIX calls you already know — there is a WASI analogue of open()/read()/write() working over file descriptors — but a module only gets to call the ones the runtime chose to hand it.
The design choice underneath WASI is the deep one, and it should ring a bell from this very rung: WASI is capability-based. In ordinary POSIX, any process can call open() on any path its credentials allow — authority is ambient, attached to the whole process. WASI inverts this: a module cannot name a path it was not given. The host grants it a handle to a specific directory or file before the module runs, and the module can only reach what it holds a handle to. This is exactly capability-based security — authority travels as an unforgeable handle, not as a global permission the code can ask for. A Wasm module handed only a handle to `/tmp/work` physically cannot read `/etc/passwd`; there is no path it can name to get there.
- Compile your C or Rust to a .wasm module that declares its imports — e.g. it imports a WASI fd_read it expects the host to supply.
- The runtime loads the module and validates the bytecode; malformed or ill-typed code is rejected before any execution.
- The host decides which capabilities to grant — say, a handle to one directory and stdout — and provides only those imports; everything else is simply absent.
- The runtime JIT-compiles the functions and runs them; every memory access is bounds-checked, and every world-touching call goes through a granted import.
Why this is a frontier: where Wasm is going beyond the browser
Put the pieces together and you see why people are excited. Wasm gives you near-native speed, true portability (the same `.wasm` runs on any host with a runtime, regardless of OS or CPU), a strong sandbox by construction, and capability-scoped access to the world. That combination quietly competes with two old tools at once. Compared to a container, a Wasm module is far smaller and starts in microseconds rather than the tens of milliseconds a process and its namespaces need — there is no OS image, no fork/exec, just "validate and JIT." Compared to a native plugin loaded with dlopen, it is safe to run code from a third party you do not trust, because a misbehaving module cannot corrupt the host.
Those properties are pulling Wasm into real systems work. On the edge and in serverless, a runtime can spin up a fresh sandboxed instance per request in microseconds, dodging the cold-start cost of booting a container — your function is a `.wasm`, not a VM. In databases and proxies, Wasm is becoming the way to run user-supplied extensions safely in-process, where a native plugin would risk crashing the whole server. And it is a clean way to ship one compiled artifact that runs identically across architectures — the portable lingua franca dream, but enforced by a sandbox rather than by hope. The Component Model, an emerging standard, pushes further: letting Wasm modules written in different languages link together with typed interfaces, so a Rust module and a C module compose without sharing a fragile ABI.