Server Side WebAssembly in February 2025, A Practical Survey
TL;DR — WASI 0.2 finally shipped a real component model. Wasmtime 28 and WasmEdge 0.14 are production-grade for cold-start-sensitive workloads. Don’t replace your container fleet yet, but plugins, edge functions, and multi-tenant isolation are unambiguous wins.
I’ve been shipping WebAssembly on servers since the Lucet days, and the field has changed more in the last twelve months than in the four years before. The component model is no longer a slide deck. WASI Preview 2 stabilized in early 2024 and runtime support caught up through the back half of the year. By the time we hit February 2025, the question is no longer “is server-side Wasm real” but “where does it actually beat the alternative.”
This survey is the answer I’d give a staff engineer who walked into my office on a Monday morning and said: pitch me Wasm, but skip the marketing. We’ll cover the runtimes, the standards, the tooling, and the workload shapes where I’d actually deploy it. I’ll save the deep dives for the rest of this series, including the cargo-component tutorial coming up next.
Some opinions, stated plainly. Wasm is not faster than native for steady-state CPU-bound code. It is dramatically better for isolation density. Cold starts are 10x to 100x faster than containers. The component model is the most important spec since WASI itself, and most teams will adopt it through a higher-level runtime rather than by hand. With that out of the way, let’s look at what’s actually running.
The Runtime Landscape
Four runtimes matter on the server in February 2025. Wasmtime, Wasmer, WasmEdge, and the embedded engines inside Spin and wasmCloud (both of which use Wasmtime under the hood). Picking one is mostly about your embedding language and your appetite for newer features.
+-------------------------------+
| Your Application |
+---------------+---------------+
|
+---------------------+---------------------+
| | |
+----v-----+ +-----v-----+ +------v------+
| wasmtime | | wasmer | | WasmEdge |
| 28.x | | 4.4 | | 0.14 |
+----+-----+ +-----+-----+ +------+------+
| | |
Cranelift Cranelift/ LLVM AOT
(JIT/AOT) LLVM/Singlepass (highest
throughput)
Wasmtime 28
The Bytecode Alliance reference. Rust embedding is first class, C/C++ via the wasmtime-c-api, Python via wasmtime-py, and so on. As of 28.x, the component model and WASI 0.2 are stable, the pooling allocator is the default for multi-tenant workloads, and resource limits per-store are mature. I default to Wasmtime unless I have a specific reason not to. Docs at wasmtime.dev are kept honest.
Wasmer 4.4
Wasmer’s value proposition has shifted toward the runtime ecosystem and wasmer.io registry. The runtime itself supports three backends (Cranelift, LLVM, Singlepass) which is useful when you want LLVM-tier optimization for steady-state workloads. Wasmer’s WASIX extends WASI with a fuller POSIX surface, which is great for porting existing apps but locks you outside the standard.
WasmEdge 0.14
CNCF sandbox. C++ runtime with the strongest LLVM AOT story, which translates to the best raw throughput I’ve measured for steady-state inference and stream-processing workloads. The Kubernetes integration via crun and runwasi is mature.
Spin 3.1 and wasmCloud 1.4
Both are application frameworks rather than bare runtimes. Spin (Fermyon) targets HTTP microservices and gives you a Rails-shaped DX over WASI 0.2. wasmCloud is a distributed actor model for Wasm components with a control plane, suitable for fleets. They each get their own article later in this series.
WASI 0.2 and the Component Model
If you’ve been waiting for “Wasm on the server” to feel coherent, WASI 0.2 is the inflection point. The pre-1.0 WASI Preview 1 was a thin Unix-shaped syscall layer. Preview 2 is the component model, which is a different beast.
Components compose. A component is a typed, sandboxed unit that exports and imports interfaces described in WIT (Wasm Interface Type). Two components written in different languages can talk to each other through WIT without sharing an ABI. That’s the headline.
Here’s the minimal WIT world I’d hand a new hire:
package example:greeter@0.1.0;
interface greet {
record person {
name: string,
title: option<string>,
}
greet: func(p: person) -> string;
}
world greeter {
export greet;
}
That gets compiled to a .wasm component. A host loads it through wasmtime::component::Component::from_file, instantiates it against a Linker, and calls greet. The host never needs to know what language the guest was written in. That decoupling is the actual product.
The canonical reference is component-model.bytecodealliance.org. Read the “Design” section before you write anything serious.
A Minimal End-to-End Example
Let’s compile a tiny component and run it from a Rust host. I’ll keep it short because each of the next articles takes a piece of this and goes deeper.
Step 1, Toolchain Setup
rustup install 1.84.0
rustup default 1.84.0
rustup target add wasm32-wasip2
cargo install cargo-component --version 0.18.0
cargo install wasm-tools --version 1.221.0
cargo install wasmtime-cli --version 28.0.0
The wasm32-wasip2 target is what you want for WASI 0.2 components. Older guides will tell you to use wasm32-wasi, which is Preview 1, which you should not use for new code in 2025.
Step 2, Generate the Component
cargo component new --lib greeter
cd greeter
Edit wit/world.wit to match the WIT above, then implement the export in src/lib.rs:
#[allow(warnings)]
mod bindings;
use bindings::exports::example::greeter::greet::{Guest, Person};
struct Component;
impl Guest for Component {
fn greet(p: Person) -> String {
match p.title {
Some(t) => format!("Hello, {} {}", t, p.name),
None => format!("Hello, {}", p.name),
}
}
}
bindings::export!(Component with_types_in bindings);
Build:
cargo component build --release
# output: target/wasm32-wasip2/release/greeter.wasm
Step 3, Run with wasmtime CLI
wasmtime run --invoke 'greet({name: "Ada", title: some("Dr.")})' \
target/wasm32-wasip2/release/greeter.wasm
You should see Hello, Dr. Ada. That’s a real WASI 0.2 component round-tripping typed values between host and guest.
Where Server Side Wasm Actually Wins
I get asked weekly whether Wasm is going to “replace containers.” Wrong frame. Here are the workload shapes where it genuinely wins in 2025.
Plugin systems. If you’re shipping a product that needs user-supplied code, Wasm is unambiguous. Sandboxed, deterministic, language-agnostic. Envoy proxy filters, Shopify Functions, and Zellij plugins are the public examples. I’ve shipped two of these in the last year and would not have used anything else.
Edge functions. Cold start under 5 milliseconds matters when you’re charging per request and routing through 200 PoPs. Cloudflare Workers, Fastly Compute, and Vercel Edge all use Wasm under the hood for non-JS runtimes. I’ll cover this in detail in the edge Wasm article.
Multi-tenant compute. When you need to densely pack thousands of customer workloads on the same VM, Wasm’s per-instance memory and the wasmtime pooling allocator let you achieve isolation density that containers can’t touch. Fermyon Cloud and wasmCloud lean into this.
Polyglot composition. WIT lets you stitch components written in Rust, JavaScript (via ComponentizeJS), Python (via componentize-py), and Go (TinyGo) into a single deployable. Not a fit for every team, but if you have a multi-language platform team, the component model is genuinely new capability.
Where it loses. Anything that wants raw syscall access (databases, kernel-adjacent agents, GPU compute), anything where the dependency wants a thousand C libraries with dlopen, and anything where steady-state CPU throughput is the only thing that matters. Run those native or in containers.
Benchmarks I Actually Trust
A quick interlude because every Wasm article should have numbers and most don’t have honest ones. Here’s what I’ve measured on representative workloads in early 2025, all on Linux x86_64, kernel 6.6, wasmtime 28 with the pooling allocator and AOT precompilation.
For a JSON transform endpoint (parse 2 KB body, run a 50-line policy, emit 1 KB), I see roughly 38,000 requests per second per core in wasmtime versus 47,000 in a native Go binary. The Wasm version pays roughly 20% throughput for the isolation. For a tighter inner loop (a parser that doesn’t touch the heap), the gap shrinks to under 10%. For workloads dominated by I/O syscalls (a proxy doing TLS termination and forwarding), Wasm is currently 2-3x slower than native because every syscall goes through a host shim. The takeaway: profile your shape before deciding. If you’re CPU-bound on math, Wasm is competitive. If you’re syscall-bound, native still wins by a lot.
Cold-start numbers are where Wasm is obviously ahead. AOT-precompiled components instantiate in 200-800 microseconds. JIT instantiation is 15-50 ms. Docker container cold-start on a warmed-up host is 80-200 ms. On a cold host (image pull) it’s seconds. For request-per-instance models (FaaS, edge, plugins), 200 microseconds vs 200 milliseconds is the difference between “we run this per request” and “we keep warm pools.” The pool-management complexity savings alone can pay for the platform migration.
Memory overhead per instance is the other quiet win. A wasmtime instance with linear memory at default page size and no I/O state runs around 1-3 MB RSS overhead. A scratch Docker container with a minimal Go binary plus the containerd-shim is 25-60 MB. Multiply across thousands of idle tenants and you see why Fermyon and Cloudflare both bet hard on Wasm for multi-tenant compute.
Common Pitfalls
The pitfalls list for new adopters in 2025 is shorter than it was, but still real.
Mixing WASI Preview 1 and Preview 2. Old wasm32-wasi modules cannot be loaded as components without wasm-tools component new adapter shims. New code should target wasm32-wasip2 directly. Mixing them in one repo causes confusing link errors. Pick one per crate.
Assuming threads work. Wasm threads are a separate proposal (threads and shared-everything-threads). They are not part of WASI 0.2’s stable surface. If your library spawns threads, it will likely fail to compile or fail at instantiation. Audit dependencies for std::thread::spawn and rayon before you commit.
Underestimating binary size. A non-trivial Rust component with tokio, serde, and reqwest analogues can hit 5-10 MB uncompressed. Use wasm-opt -Os, strip debug info, and consider no_std for hot-path components. The pooling allocator’s cold-start advantage evaporates if every cold instance pages in 10 MB.
Forgetting host capability grants. WASI 0.2 is capability-based. A guest cannot open a file unless the host explicitly grants a preopened directory. People migrate from Preview 1 and are shocked when std::fs::read returns PermissionDenied. That’s the design, not a bug.
Troubleshooting
“unknown import” at instantiation. The guest depends on a WASI interface the host didn’t link. Run wasm-tools component wit your.wasm to dump the imports, then add the matching add_to_linker calls to your host.
Cold start measurably slow. First, confirm you’re using the pooling allocator (Config::allocation_strategy(InstanceAllocationStrategy::pooling(...))). Second, pre-compile with wasmtime compile and load the .cwasm artifact. Third, profile with WASMTIME_LOG=wasmtime_jit=trace.
Component fails to load with “unknown section.” You probably built a core module (wasm32-wasi or wasm32-unknown-unknown) and are trying to load it as a component. Either rebuild with cargo component build or wrap with wasm-tools component new --adapt wasi_snapshot_preview1.wasm.
Tooling Maturity Notes
A few tooling observations that aren’t in the marketing decks but matter when you actually adopt this stuff. cargo-component reached 0.18 in January 2025 and is now stable enough that I no longer expect breakage between patch releases. The component-model docs at component-model.bytecodealliance.org caught up with the implementations in Q4 2024, which is finally the case for the first time. WIT IDE support is the laggard. There’s a tree-sitter grammar that handles syntax highlighting and a wit-bindgen LSP that’s experimental. Don’t expect IntelliJ-grade refactoring.
For observability, OpenTelemetry support across Wasm hosts is uneven. wasmtime emits traces if you instrument the host. Spin and wasmCloud emit traces natively. Cloudflare Workers and Fastly Compute have proprietary observability that’s good but isn’t OTel-native. If you’re building on the bare runtime, plan to instrument yourself with tracing on the host side and an explicit log interface in your WIT.
Debugging is the rough edge. wasmtime’s GDB integration works for core modules; component-level debugging is improving but you’ll still drop down to eprintln! more than you’d like. Time-travel debugging (a la rr) doesn’t exist yet for Wasm. For production debugging, the play is structured logs plus traces; treat the component as a black box and lean on the host for diagnostics.
Wrapping Up
Server-side Wasm in February 2025 is a real platform for the workloads it fits. The component model is the breakthrough, not raw runtime performance. If you’re evaluating it for a project this quarter, prototype on Wasmtime 28 with wasm32-wasip2, lean into WIT for your contracts, and pick the embedding shape (bare runtime, Spin, wasmCloud, or edge platform) that matches your operational model. Don’t try to migrate your whole platform at once. Pick the workload where the cold-start and density wins are clear, ship it, learn the operational surface, then expand. The rest of this series takes each embedding shape one at a time.