The Wasm Component Model on Fastly Compute
Reading WIT Back Out of a Component
October 15, 2026, by Kats Omuro (@katsuyukiomuro (opens in a new window) on X/Twitter)
The natural guess is that WIT works like a C header: paperwork for the compiler, used to generate bindings and then left behind. Ship the binary, and the description of its interface stays in your repository.
A component doesn't work that way. The service World and Two Pollables, One World both used wasm-tools component wit to list a built component's imports, and that list was only the first screen of what it prints. The binary carries real WIT, and the tool turns it back into text.
What a component remembers
wasm-tools is the Bytecode Alliance's toolkit for inspecting and transforming Wasm binaries. Run its component wit command on the example from The service World, the build with no extra features, and the output starts with the import list from that post, under a world it calls root:
package root:component;
world root {
import fastly:compute/types@0.1.0;
import fastly:compute/async-io@0.1.0;
import fastly:compute/http-body@0.1.0;
import fastly:compute/http-resp@0.1.0;
import fastly:compute/http-req@0.1.0;
...
export fastly:compute/http-incoming@0.1.0;
}
The example's own world was named fastly-compute-guest, in a package called local:main. That name didn't survive. A world is something you write bindings against, not something a component imports, so the binary doesn't record it.
What follows the world is the part worth reading: a definition for every interface on the list. Here's http-body and http-resp:
interface http-body {
use async-io.{pollable as body};
use types.{error};
new: func() -> result<body, error>;
write: func(body: borrow<body>, buf: list<u8>) -> result<u32, error>;
}
interface http-resp {
use http-body.{body};
use types.{error};
resource response {
new: static func() -> result<response, error>;
}
send-downstream: func(response: response, body: body) -> result<_, error>;
}
compute.wit's http-body has eleven functions. This one has two, new and write, because those are the two the example calls. http-resp is down to a constructor and send-downstream. async-io is a single line, resource pollable;, with no methods at all, since the example never calls one. Types come through whole when they're used: the error variant has every case, because a function the example calls can return any of them.
So what a component records isn't a copy of compute.wit. It's the exact slice the component depends on, function by function, with every type fully spelled out. That's the slice a host checks when it instantiates the component, and it's why compute.wit Changed could say that a new method like set-lookup-timeout breaks nothing: a component built before it existed doesn't import it, so the host never has to supply it.
The version numbers ride along too. Every wasi: import carries @0.2.6, the WASI point release the pinned Rust toolchain emits. When Hello, World described a real service refusing a component built by a newer Rust, with a "missing import" error, this is the line that would have shown the mismatch before deploying.
There are no doc comments in any of it. A component built by wit-bindgen keeps the shapes of its imports and nothing else.
What a core module doesn't
The contrast is a core module, the kind Hello, World found the Rust SDK producing for wasm32-wasip1. Here's one with a single import, written by hand to match how the SDK's fastly-sys declares fastly_http_body's new:
(module
(import "fastly_http_body" "new" (func $new (param i32) (result i32)))
(memory (export "memory") 1)
(func (export "_start")
(drop (call $new (i32.const 0)))))
wasm-tools component wit refuses it:
error: input is a core wasm module with no `component-type*` custom sections meaning that there is not WIT information; is the information not embedded or is this supposed to be a component?
All the binary knows about that import is a module name, a function name, and (param i32) (result i32). The i32 going in is a pointer where the host writes a body handle, and the one coming out is a status code, but nothing in the module says so. That knowledge lives in fastly-sys, on the guest side, and in the host. The error's mention of component-type sections is the other half of the story: wit-bindgen embeds its WIT in exactly those custom sections while it compiles, and the step that turns a core module into a component reads them back out.
A WIT package is a component too
wasm-tools can also go the other way. Point component wit at a directory holding compute.wit and its WASI dependencies, add --wasm, and it encodes the whole package as a binary:
00000000: 0061 736d 0d00 0100 .asm....
Those are the first eight bytes of a component, the same header Hello, World showed for a built Compute program, and there's no code in it, only types and exports. Decode it back to text and nearly everything is there, doc comments included. wasm-tools objdump shows where they went:
custom "package-docs" | 0x19be5 - 0x34283 | 108190 bytes | 1 count
About half of the 214 KB binary is documentation, kept in a custom section next to the types it describes. This is the form WIT packages travel in. wkg, the registry tool the examples' READMEs credit for fetching the WASI packages, can "build a WIT package into a component" and publish it to a registry.
A handful of doc comments don't make the round trip, and for Compute they're the wrong handful. Comparing the original against the decoded text, the missing ones are all attached to use lines:
/// An HTTP request or response body.
use async-io.{pollable as body};
The decoded package has the use line with no comment above it. The same goes for the four KV Store pending handles and the pending-request, pending-response, and pending-entry aliases. A use with an alias doesn't create a new type, only a new name for an existing one, so the encoding has nothing to hang a doc comment on. In compute.wit, those aliases are most of the names you actually work with.
Beyond the WIT
The usual way to think about this is an analogy for Windows programmers. A .wit file is less like a C header, which is gone once the compiler is done with it, and more like a COM .idl file. COM compiled .idl into a type library that could ride along inside the DLL, and a tool could read the interface back out without the source.
The analogy holds for an encoded WIT package, docs and all. For a built component it only half holds. A type library described everything a DLL offered. A component's embedded WIT describes what it needs and provides, trimmed to what it uses. That makes it less useful as documentation and more useful as a contract, and a binding can use it that way in three places.
The first is before deploying. The import list is the exact set of functions a host has to supply, version numbers included. Comparing it against the host's world, as The service World suggested, catches a missing interface or a WASI point-release mismatch before the deploy does.
The second is between versions of the binding itself. Running component wit on a program built against the old binding and the same program built against the new one, then diffing the outputs, shows exactly which host functions an upgrade starts depending on. A binding update that starts calling a newer function, like set-lookup-timeout, can't run on a host that doesn't have it, and the diff says so in one line.
The third is a caution for any binding generator that reads WIT from a binary instead of from text. The aliases keep their names but lose their doc comments, so generated documentation for body or pending-response comes out blank. Since those aliases are where compute.wit explains its most important handles, a generator that wants useful docs should read the text form.
Next: every resource in compute.wit is one the host owns, and the guest only ever holds handles to them. That's how Compute works today, but it isn't a rule of the Component Model.