The Wasm Component Model on Fastly Compute
Backend Options: Tuning and Reading Back
October 7, 2026, by Kats Omuro (@katsuyukiomuro (opens in a new window) on X/Twitter)
Dynamic Backends by Hand pointed out that the setters on dynamic-backend-options can't fail: none of them returns a result. That also means none of them can tell you whether a setting took. The only way to find out is to ask the backend afterward, and backend has a getter for almost everything a backend can be configured with.
We're jumping back to dynamic backends after two months of other topics because that's what I wanted to fill in. That post quoted only the first few setters and skipped the rest with ..., and most of the getters haven't come up at all. One case from Send Errors leads back here, too: connection-limit-reached is what a max-connections limit produces.
Reading a backend's settings back
Here are the getters that haven't appeared in this series yet:
resource backend {
...
/// Gets the “override host” for this backend.
///
/// This is used to change the `Host` header sent to the backend. See
/// [the Fastly documentation on override hosts].
///
/// [the Fastly documentation on override hosts]: https://docs.fastly.com/en/guides/specifying-an-override-host (opens in a new window)
get-override-host: func(
max-len: u64,
) -> result<option<list<u8>>, error>;
...
/// Gets the first byte timeout of the backend.
///
/// This timeout applies between the time of connection and the time we get the first byte back.
get-first-byte-timeout-ms: func() -> result<timeout-ms, error>;
/// Gets the between byte timeout of the backend.
///
/// This timeout applies between any two bytes we receive across the wire.
get-between-bytes-timeout-ms: func() -> result<timeout-ms, error>;
...
/// Gets the minimum TLS version this backend will use.
get-tls-min-version: func() -> result<option<tls-version>, error>;
/// Gets the maximum TLS version this backend will use.
get-tls-max-version: func() -> result<option<tls-version>, error>;
/// Returns the time for this backend to hold onto an idle HTTP keepalive connection
/// after it was last used before closing it.
get-http-keepalive-time: func() -> result<timeout-ms, error>;
/// Returns `true` if TCP keepalives have been enabled for this backend.
get-tcp-keepalive-enable: func() -> result<bool, error>;
/// Returns the time to wait in between sending each TCP keepalive probe to this backend.
get-tcp-keepalive-interval: func() -> result<timeout-secs, error>;
/// Returns the time to wait after the last data was sent before starting to send TCP keepalive
/// probes to this backend.
get-tcp-keepalive-probes: func() -> result<probe-count, error>;
/// Returns the time to wait after the last data was sent before starting to send TCP keepalive
/// probes to this backend.
get-tcp-keepalive-time: func() -> result<timeout-secs, error>;
}
A few details are worth a look before running them:
get-override-hostreturns bytes,option<list<u8>>, whereget-hostreturns astring. The override host becomes the value of aHostheader, and header values are bytes, as Synthesizing a Response explained.timeout-ms,timeout-secs, andprobe-countare allu32aliases in thebackendinterface. They put the unit in the type's name, which is more thanu32alone would say.- The TLS versions are
option<tls-version>, wheretls-versionis theu16from TLS'sProtocolVersion.nonemeans no limit was set. get-tcp-keepalive-probeshasget-tcp-keepalive-time's doc comment, word for word. The probes getter returns aprobe-count, the number of probes to send before the connection is considered dead, not a time.
Tuning a dynamic backend
Most getters have a matching setter on dynamic-backend-options. They fall into three groups. The first is about TLS:
/// Defines the hostname that the server certificate should declare, and turn on validation
/// during backend connections.
///
/// You should enable this if you are using TLS, and setting this will enable TLS for the
/// connection as a side effect.
///
/// If `cert-hostname` is not provided (default), the server certificate’s hostname may
/// have any value.
cert-hostname: func(value: string);
/// Sets the CA certificate to use when checking the validity of the backend.
///
/// Setting this will enable TLS for the connection as a side effect.
///
/// If `ca-certificate` is not provided (default), the backends’s certificate is validated
/// using a set of public root CAs.
ca-certificate: func(value: string);
/// Sets the acceptable cipher suites to use for TLS 1.0 - 1.2 connections.
///
/// Setting this will enable TLS for the connection as a side effect.
tls-ciphers: func(value: string);
/// Sets the SNI hostname for the backend connection.
///
/// Setting this will enable TLS for the connection as a side effect.
sni-hostname: func(value: string);
cert-hostname is the one to take seriously. Its doc comment says "You should enable this if you are using TLS," and without it the certificate's hostname isn't checked at all. Send Errors set it for the expired-certificate case, which is how that case got its tls-certificate-error.
Every one of these turns TLS on "as a side effect." So do tls-min-version and tls-max-version, and so does client-cert, which presents a client certificate whose key comes from a Secret Store secret.
The second group is about connections and how long they're reused:
/// Configures up to how long to allow HTTP keepalive connections to remain idle in the
/// connection pool.
http-keepalive-time-ms: func(value: u32);
/// Configures whether or not to use TCP keepalive on the connection to the backend.
tcp-keepalive-enable: func(value: u32);
...
/// Configures the maximum number of connections to keep in the local connection pool.
///
/// `0` is unlimited.
///
/// Note that this limit is best determined experimentally, since the total number of
/// connections to the backend will depend on POP sizes, HTTP keepalive limits, and the
/// traffic patterns for individual POPs.
max-connections: func(value: u32);
/// Configures how many times a pooled connection can be used.
///
/// `0` is unlimited.
max-use: func(value: u32);
/// Configures an upper bound for how long an HTTP keepalive connection can be open before we
/// stop trying to reuse it.
///
/// `0` is unlimited.
max-lifetime-ms: func(value: u32);
/// Determines whether or not connections to the same backend should be pooled across different
/// sandboxes.
///
/// ...
pooling: func(value: bool);
tcp-keepalive-enable takes a u32, and its getter returns a bool. In the example, 1 read back as true.
The third group is two settings that change how a connection is made at all. grpc marks the backend as one that will carry gRPC, which in practice means HTTP/2. prefer-ipv6 decides which address family to try first when a hostname has both, and the doc comment says Compute defaults to true.
Running the getters
Full working code: full example on GitHub (opens in a new window).
The example's /static path opens the static backend http_me and reads back every setting. /dynamic registers a backend with nearly every option set to something other than its default, and reads those back:
/// A dynamic backend with every tuning option set to something other than
/// its default, so the getters have something to report.
fn tuned() -> Result<backend::Backend, ()> {
let o = backend::DynamicBackendOptions::new();
o.override_host("http-me.fastly.dev");
o.connect_timeout(2_000);
o.first_byte_timeout(5_000);
o.between_bytes_timeout(3_000);
o.tls_min_version(0x0303); // TLS 1.2
o.tls_max_version(0x0304); // TLS 1.3
o.cert_hostname("http-me.fastly.dev");
o.sni_hostname("http-me.fastly.dev");
o.http_keepalive_time_ms(30_000);
o.tcp_keepalive_enable(1);
o.tcp_keepalive_interval_secs(10);
o.tcp_keepalive_probes(3);
o.tcp_keepalive_time_secs(60);
o.max_connections(50);
o.max_use(100);
o.max_lifetime_ms(60_000);
o.prefer_ipv6(false);
backend::register_dynamic_backend("tuned", "http-me.fastly.dev", o).map_err(|_| ())
}
There's no use-tls(true) in there. The TLS options are supposed to turn it on by themselves.
On a real Compute service, /static reports the platform's defaults for a backend that was set up with only an address and a port. (The Fastly CLI fills in the override host and the TLS hostnames from the address, which is why an override host shows up.)
name: http_me
is-dynamic: Ok(false)
host: Ok("http-me.fastly.dev")
override-host: Ok(Some("http-me.fastly.dev"))
port: Ok(443)
is-tls: Ok(true)
tls-min-version: Ok(None)
tls-max-version: Ok(None)
connect-timeout-ms: Ok(1000)
first-byte-timeout-ms: Ok(15000)
between-bytes-ms: Ok(10000)
http-keepalive-time: Ok(600000)
tcp-keepalive-enable: Ok(true)
tcp-keepalive-interval:Ok(10)
tcp-keepalive-probes: Ok(3)
tcp-keepalive-time: Ok(300)
is-healthy: Ok(BackendHealth::Unknown)
The timeouts match the defaults dynamic-backend-options documents for its own setters. The HTTP keepalive time is ten minutes, and TCP keepalive is on.
/dynamic reads back what was set, with TLS on and the port at 443, even though nothing called use-tls:
name: tuned
is-dynamic: Ok(true)
host: Ok("http-me.fastly.dev")
override-host: Ok(Some("http-me.fastly.dev"))
port: Ok(443)
is-tls: Ok(true)
tls-min-version: Ok(Some(771))
tls-max-version: Ok(Some(772))
connect-timeout-ms: Ok(2000)
first-byte-timeout-ms: Ok(5000)
between-bytes-ms: Ok(3000)
http-keepalive-time: Ok(30000)
tcp-keepalive-enable: Ok(true)
tcp-keepalive-interval:Ok(10)
tcp-keepalive-probes: Ok(3)
tcp-keepalive-time: Ok(60)
is-healthy: Ok(BackendHealth::Unknown)
771 and 772 are 0x0303 and 0x0304, TLS 1.2 and 1.3. Neither backend has a health check, so is-healthy says unknown for both.
Viceroy tells a different story. Every getter from tls-min-version through tcp-keepalive-time answers Err(Error::Unsupported), on both backends. The timeout getters are tracked as fastly/Viceroy#723 (opens in a new window), which also notes that the Rust SDK treats that error as a bug and panics. And on the dynamic backend, Viceroy reports is-tls: false and port 80, because it didn't apply the TLS side effect. Several of the TLS options are among the dynamic backend settings fastly/Viceroy#342 (opens in a new window) lists as unimplemented. The TLS-version and keepalive getters, and the missing side effect, don't seem to be ticketed yet.
Beyond the WIT
Setters that can't fail move the question of whether a setting took somewhere else, and a binding gets to choose where.
The Rust SDK's BackendBuilder answers part of it by not relying on the host. Each builder method for a setting the WIT says turns TLS on "as a side effect" (check_certificate, sni_hostname, the TLS version and cipher settings, the CA certificate, and the client certificate) also sets a TLS flag in the guest. When the component build hands the options to the host, it calls use-tls(true) itself whenever that flag is set. A program built on the SDK wouldn't have hit Viceroy's missing side effect, because the SDK never depended on it. That's a sound rule for any binding: when the ABI documents an implicit effect, make the call explicit, and the binding stops depending on every host implementing the implicit part.
The getters are the check for everything else. After registering a backend, a binding or a test can read back the settings that matter and compare, which is how the example caught Viceroy ignoring the side effect. The Rust SDK turns any getter error into a panic, which #723 describes, and that's harsh for local development: the one tool for confirming a configuration is the thing that brings the program down. A getter that returns the error lets a test report "unsupported here" and move on.
The getters are also where a wrapper can go wrong without anything noticing. In the SDK's component build, Backend::get_http_keepalive_time calls the WIT's get-tcp-keepalive-time, not get-http-keepalive-time, and reads the result as milliseconds. For the dynamic backend above, that would report an HTTP keepalive time of 60 milliseconds (the TCP keepalive time, which is in seconds) instead of 30 seconds. Nothing catches it, because timeout-ms and timeout-secs are both aliases of u32. The WIT puts the unit in the name, but an alias isn't a separate type, so a binding that wants its compiler to catch a mix-up like this has to give the two units distinct types of its own.
Next: health checks, the one part of a backend's configuration whose setters can fail, and whose rules its defaults break.