The Wasm Component Model on Fastly Compute
Environment: The Interface That Isn't Fastly-Specific At All
August 17, 2026, by Kats Omuro (@katsuyukiomuro (opens in a new window) on X/Twitter)
EDIT (2026-08-18): added a note on how the Rust SDK reaches environment variables on its own wasm32-wasip1 build.
Every interface met so far has lived under fastly:compute—http-req, config-store, kv-store, all of it Fastly's own design. So when you want to know something basic about the request you're handling, like which POP it landed on, the natural guess is a Fastly interface for that too. There is one (next up), but it isn't the first stop. The first stop is standard WASI, with zero Fastly awareness baked in.
Command-line programs in WASI
compute.wit's service-imports world pulls in a whole family of wasi:cli interfaces:
world service-imports {
...
import wasi:cli/environment@0.2.6;
import wasi:cli/exit@0.2.6;
import wasi:cli/stdout@0.2.6;
import wasi:cli/stderr@0.2.6;
import wasi:cli/stdin@0.2.6;
...
}
wasi:cli is the package WASI defines for command-line programs—argv, environment variables, stdio, an exit code, the pieces a plain POSIX process would expect. Because we pull in parts of that package, it looks like it should make a Compute service a command-line program too, at the WASI level, but that's not what it means.
The thing that actually makes a component a wasi:cli command is exporting wasi:cli/run—a generic entry point, called once, playing the same role main does for a real process.
A Compute service is a reactor
compute.wit's service world never exports wasi:cli/run. The only thing it exports is fastly:compute/http-incoming, and Hello, World already noticed the shape this leaves: compute.wit imports individual wasi:cli interfaces one at a time, never the bundled command world they'd normally travel in.
A component that exports specific functions to be called by other programs, instead of one generic entry point a shell would invoke, is what the WASI world calls a reactor—closer to a library than a command-line program. That's what a Compute service actually is.
println! resolving to wasi:cli/stdout, which Hello, World also leaned on without dwelling on it, doesn't care either way—stdio works the same for a command-line program or a reactor. environment sits on the same import list for the same reason: it's a plain, portable WASI interface, not something reserved for commands. Its three functions just don't all apply evenly. get-environment means exactly as much to a reactor as to a command. get-arguments and initial-cwd don't—they answer questions that only make sense for something invoked like a process in the first place, and a Compute service never is. They're on the interface because wasi:cli/environment bundles all three together, not because Fastly expects either one to say anything.
None of that changes what actually happens with the piece that is meaningful here. Fastly doesn't invent a new mechanism to hand a reactor its runtime context, either. It hands your service environment variables the same way it would hand a command-line process them—same interface, same shape—and happens to be the one deciding what those variables say.
The interface itself
This comes from the wasi-cli (opens in a new window) spec rather than compute.wit itself, since the interface is standard WASI, not Fastly's—specifically the wasi:cli/environment@0.2.6 package, the exact version compute.wit's service-imports world pins above:
@since(version = 0.2.0)
interface environment {
/// Get the POSIX-style environment variables.
///
/// Each environment variable is provided as a pair of string variable names
/// and string value.
///
/// Morally, these are a value import, but until value imports are available
/// in the component model, this import function should return the same
/// values each time it is called.
@since(version = 0.2.0)
get-environment: func() -> list<tuple<string, string>>;
/// Get the POSIX-style arguments to the program.
@since(version = 0.2.0)
get-arguments: func() -> list<string>;
/// Return a path that programs should use as their initial current working
/// directory, interpreting `.` as shorthand for this.
@since(version = 0.2.0)
initial-cwd: func() -> option<string>;
}
Three functions, and get-environment is the one that matters here: no key parameter, no lookup—you get the whole list back in one call. That doc comment explains why: environment variables aren't really meant to be a function call at all. They're what the component model calls a value import, something that's just... there, fixed, before your code runs a single instruction. The Component Model doesn't have value imports yet, so wasi:cli fakes one with a function that's contractually required to return the same answer every time. Contrast that with Config Store's get, which takes a key and can return a different answer between two calls to the same store—get-environment can't, by the interface's own promise.
What Fastly Compute puts in it
Fastly Compute defines nine FASTLY_* variables:
FASTLY_TRACE_IDFASTLY_HOSTNAMEFASTLY_POPFASTLY_REGIONFASTLY_CACHE_GENERATIONFASTLY_CUSTOMER_IDFASTLY_IS_STAGINGFASTLY_SERVICE_IDFASTLY_SERVICE_VERSION
Every one of these is readable with nothing but get-environment. For details on the individual environment variables, see Environment variables reference for the Compute platform (opens in a new window) in Fastly Compute documentation.
Reading it directly
Full working code: full example on GitHub (opens in a new window).
The handler filters get-environment() down to the FASTLY_ prefix and prints get-arguments()/initial-cwd() alongside it:
mod bindings;
use bindings::{exports::fastly::compute::http_incoming, fastly::compute::{http_body, http_resp}, wasi::cli::environment};
struct EnvironmentExample;
impl http_incoming::Guest for EnvironmentExample {
fn handle(_request: http_incoming::Request, _request_body: http_body::Body) -> Result<(), ()> {
let mut lines = String::new();
lines.push_str("FASTLY_* environment variables:\n");
let mut fastly_vars: Vec<(String, String)> = environment::get_environment()1
.into_iter()
.filter(|(k, _)| k.starts_with("FASTLY_"))
.collect();
fastly_vars.sort();
for (k, v) in &fastly_vars {
lines.push_str(&format!(" {k} = {v}\n"));
}
lines.push_str(&format!("\nget-arguments(): {:?}\n", environment::get_arguments()));2
lines.push_str(&format!("initial-cwd(): {:?}\n", environment::initial_cwd()));3
let response = http_resp::Response::new().map_err(|_| ())?;
response
.insert_header("content-type", b"text/plain")
.map_err(|_| ())?;
let out_body = http_body::new().map_err(|_| ())?;
http_body::write(&out_body, lines.as_bytes()).map_err(|_| ())?;
http_resp::send_downstream(response, out_body).map_err(|_| ())?;
Ok(())
}
}
bindings::export!(EnvironmentExample with_types_in bindings);
Nothing Fastly-specific about this call—it's the same wasi:cli/environment.get-environment any WASI command-line program would call. The FASTLY_ filter is just this example picking the entries worth printing out of the full list.
Included mainly to show what's not there. A Compute service is a reactor, not a wasi:cli command, and nothing ever invokes it with real command-line arguments or a real working directory, so both come back essentially empty under Viceroy. Confirmed below.
Against Viceroy:
FASTLY_* environment variables:
FASTLY_CACHE_GENERATION = 0
FASTLY_CUSTOMER_ID = 0000000000000000000000
FASTLY_HOSTNAME = localhost
FASTLY_IS_STAGING = 0
FASTLY_POP = XXX
FASTLY_REGION = Somewhere
FASTLY_SERVICE_ID = 0000000000000000000000
FASTLY_SERVICE_VERSION = 0
FASTLY_TRACE_ID = 00000000000000000000000000000000
get-arguments(): ["compute-app"]
initial-cwd(): None
All nine variables show up, filled with placeholder values Viceroy invents for local testing rather than the real per-request values a deployed service would see. get-arguments() comes back with one entry, "compute-app", standing in for argv[0] on something that was never actually invoked as a wasi:cli command—a reactor doesn't get handed a real argv to report. initial-cwd() is none—there's no filesystem underneath a Compute service for a working directory to mean anything about, which lines up with wasi:filesystem sitting in the WIT dependency graph unused, as Hello, World found for the same reason.
Beyond the WIT
Every language that targets this platform answers the same question, one way or another: what does "read an environment variable" mean with no OS process underneath, just a WASI import that's contractually frozen for the sandbox's lifetime?
For a language whose standard library already speaks WASI natively, there's no question to answer—Rust's std::env::var and C++'s getenv, built against this target, already bottom out in wasi:cli/environment.get-environment() themselves. A managed runtime with its own idea of what an environment variable API looks like, like Node.js's process.env, or even .NET's System.Environment.GetEnvironmentVariables(), may need a binding author to do that mapping by hand instead.
Fastly's own Rust SDK is the clean case here, by omission. It has no environment module of any kind. There's a compute_runtime module, which is the next post's subject, and nothing whatsoever wrapping FASTLY_* as strings, because std::env::var already covers that and a wrapper would have nothing to add. Which WASI interface the call bottoms out in does depend on the target, and that SDK's supported build is wasm32-wasip1, where it lands on wasi_snapshot_preview1's environ_get rather than the wasi:cli/environment quoted above. It's the same line in your source and the same nine variables, with different plumbing one layer underneath.
Either way, the mapping holds up better than it sounds: get-environment's doc comment promises a snapshot, fixed for the sandbox's lifetime, and that's already how most environment-variable APIs behave in practice—read once, treated as effectively immutable afterward. The write side is where you'd expect a mapping like that to break, and it's worth checking rather than assuming: Fastly's own environment variables reference (opens in a new window) says plainly that environment variables "are writable, and will retain any change written to them through the end of the sandbox lifecycle." Nothing in wasi:cli/environment itself offers a way to do that, since there's no setter anywhere on the interface, so a binding that honors a call like C++'s setenv or .NET's Environment.SetEnvironmentVariable has to keep its own guest-side copy, seeded once from get-environment(), and mutate that instead. The write sticks around for as long as the sandbox does. It just never reaches the host, another sandbox, or a future one, because there's no ABI call that would let it.
get-arguments() doesn't map to anything a Compute developer would recognize, in any language. No Compute service is invoked with a real command line, so whatever that language calls its argv equivalent has nothing real to return beyond that one placeholder entry. It's a small detail, but it's the same lesson FASTLY_IS_STAGING teaches at a bigger scale: everything this interface hands you arrives as a string, because that's what an environment variable is. There is a typed bool for a staging check and a typed u64 for a service version, and neither of them lives here. That's what compute-runtime is for, next.