The Wasm Component Model on Fastly Compute

Compute Runtime: Typed Access to Environment Data

Last time, every piece of Fastly context worth having—POP, hostname, service ID, whether this is staging—turned out to already be sitting in wasi:cli/environment.get-environment(), no Fastly-specific interface required. So why does compute-runtime exist at all? The easy guess is that it's redundant with what get-environment already hands you. Mostly, it's the same data. What it isn't is the same shape.

The whole interface

WIT
/// Features for interacting with the Compute runtime.
interface compute-runtime {
  /// A timestamp in milliseconds.
  type vcpu-ms = u64;
  /// An amount of memory in mebibytes (2^20 bytes).
  type memory-mib = u32;


  /// Gets the amount of vCPU time that has passed since this sandbox was started, in
  /// milliseconds.
  ///
  /// This function returns only time spent running on a vCPU, and does not include time spent
  /// performing any I/O operations. However, it is based on clock time passing, and so will include
  /// time spent executing hostcalls, is heavily affected by what core of what CPU is running the
  /// code, and can even be influenced by the state of the CPU.
  ///
  /// As a result, this function *should not be used in benchmarking across runs*. It can be used,
  /// with caution, to compare the runtime of different operations within the same sandbox.
  get-vcpu-ms: func() -> vcpu-ms;

  /// Get a snapshot of the current dynamic memory usage, rounded up to the nearest mebibyte (2^20).
  ///
  /// This includes usage from the Wasm linear memory (heap) and usage from host allocations
  /// made on behalf of this sandbox, e.g. buffered bodies of HTTP responses.
  /// The returned value is just a snapshot- it can change without any explicit action
  /// by the sandbox (for instance, additional response data coming in from an HTTP response.)
  /// It can also change over time / across runs, as the Compute platform's memory usage
  /// changes. Consider the returned value with these uncertainties in mind.
  get-heap-mib: func() -> memory-mib;

  /// A UUID generated by Fastly for each sandbox.
  ///
  /// This is often a useful value to include in log messages, and also to send to upstream
  /// servers as an additional custom HTTP header, allowing for straightforward correlation of
  /// which sandbox processed a request to requests later processed by an origin server.
  ///
  /// By default, each sandbox handles exactly one downstream request, in which case
  /// this sandbox UUID is unique for each request. However, by using
  /// `http-downstream.next-request`, a single sandbox can accept multiple downstream
  /// requests. For a UUID that reliably identifies a request, you may wish to use
  /// `http-downstream.downstream-client-request-id`.
  ///
  /// Equivalent to the "FASTLY_TRACE_ID" environment variable.
  get-sandbox-id: func() -> string;

  /// The hostname of the Fastly cache server which is executing the current sandbox, for
  /// example, `cache-jfk1034`.
  ///
  /// Equivalent to the "FASTLY_HOSTNAME" environment variable and to [`server.hostname`] in VCL.
  ///
  /// [`server.hostname`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-hostname/ (opens in a new window)
  get-hostname: func() -> string;

  /// The three-character identifying code of the [Fastly POP] in which the current service
  /// instance is running.
  ///
  /// Equivalent to the "FASTLY_POP" environment variable and to [`server.datacenter`] in VCL.
  ///
  /// [Fastly POP]: https://www.fastly.com/documentation/guides/concepts/pop/ (opens in a new window)
  /// [`server.datacenter`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-datacenter/ (opens in a new window)
  get-pop: func() -> string;

  /// A code representing the general geographic region in which the [Fastly POP] processing the
  /// current Compute sandbox resides.
  ///
  /// Equivalent to the "FASTLY_REGION" environment variable and to [`server.region`] in VCL, and
  /// has the same possible values.
  ///
  /// [`server.region`]: https://www.fastly.com/documentation/reference/vcl/variables/server/server-region/ (opens in a new window)
  /// [Fastly POP]: https://www.fastly.com/documentation/guides/concepts/pop/ (opens in a new window)
  get-region: func() -> string;

  /// The current cache generation value for this Fastly service.
  ///
  /// The cache generation value is incremented by [purge-all operations].
  ///
  /// Equivalent to the "FASTLY_CACHE_GENERATION" environment variable and to
  /// [`req.vcl.generation`] in VCL.
  ///
  /// [purge-all operations]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ (opens in a new window)
  /// [`req.vcl.generation`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-vcl-generation/ (opens in a new window)
  get-cache-generation: func() -> u64;

  /// The customer ID of the Fastly customer account to which the currently executing Fastly
  /// service belongs.
  ///
  /// Equivalent to the "FASTLY_CUSTOMER_ID" environment variable and to [`req.customer_id`] in VCL.
  ///
  /// [`req.customer_id`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-customer-id/ (opens in a new window)
  get-customer-id: func() -> string;

  /// Whether the request is running in the Fastly service's [staging environment].
  ///
  /// `false` for production or `true` for staging.
  ///
  /// Equivalent to the "FASTLY_IS_STAGING" environment variable and to [`fastly.is_staging`] in VCL.
  ///
  /// [`fastly.is_staging`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/fastly-is-staging/ (opens in a new window)
  /// [staging environment]: https://docs.fastly.com/products/staging (opens in a new window)
  get-is-staging: func() -> bool;

  /// The identifier for the Fastly service that is processing the current request.
  ///
  /// Equivalent to the "FASTLY_SERVICE_ID" environment variable and to [`req.service_id`] in VCL.
  ///
  /// [`req.service_id`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-service-id/ (opens in a new window)
  get-service-id: func() -> string;

  /// The version number for the Fastly service that is processing the current request.
  ///
  /// Equivalent to the "FASTLY_SERVICE_VERSION" environment variable and to [`req.vcl.version`]
  /// in VCL.
  ///
  /// [`req.vcl.version`]: https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/req-vcl-version/ (opens in a new window)
  get-service-version: func() -> u64;

  /// This function is not suitable for general-purpose use.
  get-namespace-id: func() -> string;
}

There are twelve functions, all with no parameters, no error cases—they're the simplest shape a resource-free interface can have. Nine of them say "Equivalent to the FASTLY_X environment variable" right in the doc comment, confirming the same list last time put together indirectly, from the compute-runtime side instead of the wasi:cli/environment side. Several also name the matching VCL variable, for anyone porting logic from a Fastly VCL service.

What a typed getter buys you

get-environment() hands back list<tuple<string, string>>. Every value in it, no matter what it represents underneath, is a string. get-is-staging returns bool. get-cache-generation and get-service-version return u64. That's the whole pitch: compute.wit already knows these values aren't strings at the source, so compute-runtime hands them over as what they actually are instead of making every caller re-parse "true"/"false" or "0" by hand.

Three functions in this interface have no FASTLY_* equivalent at all, because there's no static value for an environment variable to hold: get-vcpu-ms and get-heap-mib change from one call to the next within a single request, so baking either into a value fixed for the sandbox's lifetime wouldn't even make sense. get-namespace-id is the odd one out for a different reason—its entire doc comment is a single line warning it off: "This function is not suitable for general-purpose use." compute.wit doesn't say what it's for instead, so there's nothing more specific to report here beyond quoting that warning and leaving the function alone.

Reading it directly

Full working code: full example on GitHub (opens in a new window). The handler just calls all twelve functions and prints the results:

Rust

mod bindings;

use bindings::{exports::fastly::compute::http_incoming, fastly::compute::{compute_runtime, http_body, http_resp}};

struct ComputeRuntimeExample;

impl http_incoming::Guest for ComputeRuntimeExample {
    fn handle(_request: http_incoming::Request, _request_body: http_body::Body) -> Result<(), ()> {
        let mut lines = String::new();

        lines.push_str(&format!("get-vcpu-ms():          {}\n", compute_runtime::get_vcpu_ms()));
        lines.push_str(&format!("get-heap-mib():         {}\n", compute_runtime::get_heap_mib()));
        lines.push_str(&format!("get-sandbox-id():       {}\n", compute_runtime::get_sandbox_id()));
        lines.push_str(&format!("get-hostname():         {}\n", compute_runtime::get_hostname()));
        lines.push_str(&format!("get-pop():              {}\n", compute_runtime::get_pop()));
        lines.push_str(&format!("get-region():           {}\n", compute_runtime::get_region()));
        lines.push_str(&format!("get-cache-generation(): {}\n", compute_runtime::get_cache_generation()));
        lines.push_str(&format!("get-customer-id():      {}\n", compute_runtime::get_customer_id()));
        lines.push_str(&format!("get-is-staging():       {}\n", compute_runtime::get_is_staging()));
        lines.push_str(&format!("get-service-id():       {}\n", compute_runtime::get_service_id()));
        lines.push_str(&format!("get-service-version():  {}\n", compute_runtime::get_service_version()));
        lines.push_str(&format!("get-namespace-id():     {}\n", compute_runtime::get_namespace_id()));


        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!(ComputeRuntimeExample with_types_in bindings);

Against Viceroy:

Terminal output
get-vcpu-ms():          0
get-heap-mib():         2
get-sandbox-id():       00000000000000000000000000000000
get-hostname():         localhost
get-pop():              XXX
get-region():           Somewhere
get-cache-generation(): 0
get-customer-id():      0000000000000000000000
get-is-staging():       false
get-service-id():       0000000000000000000000
get-service-version():  0
get-namespace-id():

Line these up against last time's get-environment() output and the "Equivalent to" claims hold up character for character: get-sandbox-id()'s string of zeros matches FASTLY_TRACE_ID, get-hostname()'s localhost matches FASTLY_HOSTNAME, get-pop()'s XXX matches FASTLY_POP, and so on down the list. The one place they visibly diverge is get-is-staging() returning the bare word false where FASTLY_IS_STAGING printed the string "0"—same underlying value, different representation, which is the typed-versus-string distinction this interface exists to fix in the first place. get-namespace-id() comes back empty under Viceroy; nothing in its one-line doc comment suggests what a populated value would even look like.

The real fastly Rust SDK skips the by-hand bindgen calls above entirely. Its own compute_runtime module (opens in a new window) wraps eleven of these twelve functions as plain, snake_cased free functions—pop(), hostname(), is_staging(), service_id(), elapsed_vcpu_ms(), and so on—each one already returning the same typed value compute.wit promises, with no Result to unwrap because nothing here can fail at the ABI level. (get-namespace-id doesn't get a wrapper at all, which lines up with its own doc comment's warning.) Rust has no property syntax to reach for, so the SDK's answer to "how do I expose a typed nullary getter" is the plainest one available: a bare function call that already reads like fetching a value, fastly::compute_runtime::pop() rather than a builder or a resource method.

Beyond the WIT

compute-runtime is the first interface in this series where a binding genuinely has to invent API surface rather than translate an existing one: Environment variables map onto whatever environment-variable API a language already has, but compute-runtime doesn't have an obvious existing target to fall back on in most standard libraries. For example, there's no built-in concept of "which POP is this running in" waiting to receive get-pop()'s result, so a binding has to design that surface from scratch instead of just adapting one.

The shape question is less of a problem than it looks, though, because every one of these twelve functions takes no parameters and returns a single value with no error case.

Rust settled for a bare function because it had nothing else to reach for. A language with property syntax—C#, Kotlin, Swift—has an alternative, and it's exactly the shape a read-only property wants: context.Pop, context.IsStaging, context.ServiceVersion, no parentheses, no error to unwrap. Contrast that against a resource method like response.insert_header, which takes arguments and can fail—that one has to stay a method in any idiomatic wrapper, because properties in those languages don't take parameters or represent an ABI-level result<_, error> cleanly.

get-vcpu-ms and get-heap-mib are the interesting edge case inside this same interface: they look identical in shape to the rest, parameterless getters returning a plain value, but their doc comments explicitly warn that the value changes from call to call and shouldn't be treated as fixed. A property reads, by convention, as cheap and stable to access repeatedly, in any language that has them.

Does the convention survive contact with a value that's quietly mutable underneath? That's the kind of judgment call a WIT-to-property mapping has to make interface by interface, not mechanically for every parameterless getter—a question worth revisiting once there's a fuller picture of how idiomatic wrappers handle the rest of this ABI.