The Wasm Component Model on Fastly Compute
Publishing to Compute
October 26, 2026, by Kats Omuro (@katsuyukiomuro (opens in a new window) on X/Twitter)
SUCCESS from fastly compute publish sounds like the end of a deploy, but actually, it's closer to the middle. By the time it prints, the platform has checked the package twice, at two different steps that may refuse different things. After that, the new version still has to reach the edge. Most examples in this series have been published to a real Compute service, because that's where they have to work, but in most of them we didn't go into that much detail on that aspect. Today is when we're taking that dive to look a little deeper into publishing.
One fastly.toml, two hosts
Here's the fastly.toml from Two Pollables, One World, which calls a backend:
# This file describes a Fastly Compute package. To learn more visit:
# https://www.fastly.com/documentation/reference/compute/fastly-toml (opens in a new window)
authors = [""]
description = "Waiting on wasi:io's pollable and on async-io's, and why they don't mix"
language = "other"
manifest_version = 3
name = "two-pollables"
service_id = ""
[local_server]
[local_server.backends.http_me]
url = "https://http-me.fastly.dev"
override_host = "http-me.fastly.dev"
[scripts]
build = "cargo build --target wasm32-wasip2 && mkdir -p bin && cp target/wasm32-wasip2/debug/two_pollables.wasm bin/main.wasm"
[setup]
[setup.backends.http_me]
address = "http-me.fastly.dev"
port = 443
The top-level service_id ties the directory to that service. It starts empty, and when the first publish creates a service, the CLI writes the new ID into the file. From then on, CLI operations performed in that directory apply to that service.
The backend is described twice: once for the real service, and once as a stand-in for local testing.
[setup] is for the real service. Its reference documentation (opens in a new window) says it's used "when a deployment of a package requires a Fastly service to be created" (in other words, when service_id isn't set yet). It also says that "once a Compute project is associated with a Fastly service, the [setup] data is no longer used." So it creates http_me exactly once, on the first publish. Changing it afterward changes nothing. A backend you add later needs to goes through fastly backend create and a new service version, like any other change to the service.
[local_server] is for fastly compute serve, and Viceroy reads it on every run. Viceroy has no service to configure, so everything a real service would hold has to be faked here: backends, and also stores, ACLs, shielding sites, geolocation data, and device detection results.
What publishing checks
fastly compute publish runs the build script, packages bin/main.wasm with fastly.toml into pkg/<name>.tar.gz, uploads it as a new service version, and activates that version. The CLI prints a line for each step. Here's how it ended for The Same Tally, in JavaScript:
✓ Verifying fastly.toml
✓ Uploading package
✗ Activating service (version 2)
Uploading and activating are the two steps that can refuse a component, and they check different things:
- The upload. In this step, the platform reads the package and the component's metadata, and rejects what it can't make sense of. The MoonBit build with UTF-16 strings failed here with "Failed to extract metadata from package." The service keeps the version that was already active. The new version number is used up all the same: the MoonBit sandbox lists version 2 as inactive, the one whose upload failed.
- Activation. In this step, the platform compiles the component and checks it against the host's capabilities, and the error comes from the compiler,
xqd-codegen. Every import has to be in theserviceworld at the same version: The service World'swasi:httpbuild failed here, and so did the@0.2.4build from From Core Module to Component. The component also has to fit the host's limits. The JavaScript build failed both ways, with "missing import namedwasi:io/poll@0.2.12" and "The component transitively contains 3 Wasm linear memories, which exceeds the configured maximum of 2." Here too, the version that was already active stays active.
Each of those builds ran under fastly compute serve, because Viceroy runs neither check. A local run tells you the component's logic works. It doesn't tell you the component will ship.
The third place a component can fail is in handling requests, after activation, and most of that looks the same locally: a trap, or an error result. The exceptions are the limits Viceroy doesn't enforce, and those get their own post.
Waiting for the edge
Publishing version 2 of a test service took 12 seconds, ending in this:
SUCCESS: Deployed package (service RZwLZ7kY7KDFRiSj99G2FF, version 2)
The new version answered its first request about 40 seconds later. Until then, every request got version 1's answer. The CLI's last step before printing that line was "Checking service availability," and it passed, but only because version 1 was still answering.
compute-runtime has a function that settles which version you're talking to. On a real service, get-service-version returns the active version number, so a route that reports it tells you whether a deploy has reached the POP you're hitting. This one reports get-pop too:
service version 2, pop ITM
(ITM is the Itami POP, in Japan. I happen to be located in Japan.)
Under Viceroy, which has no service versions, Compute Runtime found it returns 0.
If you're testing a change, wait for the version you just published before you believe any result. For the first few seconds after it appears, a request can still land on a server that hasn't caught up, so an odd result straight after a deploy is worth repeating before you debug it.
While you're testing, it can help to log the current service version, or return it in a response header, so every response tells you which version produced it.
Reading the logs
On a real service, anything the guest writes to stderr goes to fastly log-tail:
stderr | 106d325a | tally: created requests
stderr | 106d325a | tally: created bytes
stderr | 106d325a | tally: dropped bytes at 42
stderr | 106d325a | service: dropped its handle to bytes
stderr | 106d325a | tally: dropped requests at 1
Each line carries the stream it came from and an ID for the request, so lines from concurrent requests can be told apart. The first log-tail on a service turns on managed logging, and the first lines can take a few minutes to show up. On the MoonBit sandbox, nothing arrived for the first several minutes of requests, and then lines started coming through.
Under Viceroy, the same lines appear in the terminal running fastly compute serve, between Viceroy's log lines:
2026-10-25T17:15:59.679572Z INFO request{id=0}: handling request GET http://127.0.0.1:7676/
tally: created requests
tally: created bytes
tally: dropped bytes at 42
service: dropped its handle to bytes
tally: dropped requests at 1
2026-10-25T17:15:59.680345Z INFO request{id=0}: guest completed using 1.2 MB of WebAssembly heap
2026-10-25T17:15:59.680367Z INFO request{id=0}: guest completed in 741µs
2026-10-25T17:15:59.680451Z INFO request{id=0}: response status: 200
Viceroy's own lines carry a request ID, but the guest's lines carry nothing, not even which stream they came from.
Beyond the WIT
For anyone building a binding or a toolchain for Compute, the publish is the test that counts, and it runs checks the WIT never mentions. Viceroy ran every build that failed them. Each rejection this series has hit can be caught on the binary before publishing, with a tool you already have:
- Imports.
wasm-tools component witlists them. Every one has to be in theserviceworld, version number included, or activation fails. - Memories. In
wasm-tools printoutput, a memory a core module defines prints on its own line, as(memory (;0;) 17)or similar. Imports, exports, and canonical options mention memories too, so count only the definitions:wasm-tools print bin/main.wasm | grep -cE '^\s*\(memory \(;'. More than two fails activation. - String encodings. The same output shows each canonical function's
string-encoding=option. Anything other thanutf8failed the upload the one time it was tried.
These cover the rejections this series has seen, not every one a real service can make. They're worth running so the first publish isn't where you find out, and a component that passes them still hasn't shipped until a real service answers with the version you published.
Next: what else a local run can't tell you, from the limits Viceroy doesn't enforce to the functions that only answer on one side.