Skip to main content
Version: v2

Cluster Hosts (Washlet)

Run a wasmCloud host as a cluster node connected to the operator mesh.​

A washlet is a cluster-connected host: a ClusterHost from the wash_runtime::washlet module. Unlike a standalone Host (covered in Building Custom Hosts), a washlet connects to the wasmCloud operator mesh via NATS and receives workload commands from the scheduler. This is how the Kubernetes Operator deploys and manages workloads across a fleet of hosts.

Standalone host vs. cluster host​

Standalone HostClusterHost (washlet)
Managed byYour application codewasmCloud operator via NATS
Workload commandsProgrammatic API (HostApi)NATS subjects
PluginsIn-memory (dev) or customNATS-backed for production
Entry pointHostBuilderClusterHostBuilder
Use caseEmbedding, custom integrationOperator-managed fleet

Running with wash host​

The simplest way to run a washlet is the wash host CLI command. It connects to NATS and joins a host group, ready to receive workload commands from the operator.

shell
wash host --host-group my-cluster --scheduler-nats-url nats://nats:4222

wash host flags​

FlagDefaultDescription
--host-groupdefaultHost group label used for workload scheduling
--host-name(auto-generated)Human-readable name for this host
--scheduler-nats-urlnats://localhost:4222NATS URL for the control plane (workload commands)
--data-nats-urlnats://localhost:4222NATS URL for the data plane (plugin backends)
--http-addr(disabled)If set, enables the HTTP handler on this address
--postgres-url(disabled)If set, enables the wasmcloud:postgres plugin (direct queries and prepared statements)
--wasi-webgpufalseEnable the WebGPU interface
--allow-insecure-registriesfalseAllow pulling from insecure OCI registries
--registry-pull-timeout30sTimeout for OCI pulls
--scheduler-tls-ca—TLS CA for scheduler NATS
--data-tls-ca—TLS CA for data plane NATS

This table covers the core cluster flags; for the full and current flag surface, including the probe, drain, and memory flags and the socket and filesystem policy flags, see the wash host command reference.

Environment variables​

WASH_POSTGRES_URL can be used in place of --postgres-url.

Embedding programmatically​

Use ClusterHostBuilder to embed a washlet in your own application. The builder mirrors HostBuilder but adds NATS-specific configuration:

rust
use std::sync::Arc;
use wash_runtime::{
    host::http::{Ingress, DynamicRouter},
    plugin::{
        wasi_blobstore::NatsBlobstore,
        wasi_config::DynamicConfig,
        wasi_keyvalue::NatsKeyValue,
        wasi_logging::TracingLogger,
        wasmcloud_messaging::NatsMessaging,
    },
    washlet::{ClusterHostBuilder, run_cluster_host},
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt::init();

    // Connect to NATS (scheduler and data plane can be separate connections)
    let scheduler_nats = async_nats::connect("nats://localhost:4222").await?;
    let data_nats = async_nats::connect("nats://localhost:4222").await?;

    // Build the cluster host with NATS-backed plugins
    let mut builder = ClusterHostBuilder::default()
        .with_host_group("my-cluster")
        .with_nats_client(Arc::new(scheduler_nats))
        .with_plugin(Arc::new(DynamicConfig::new(true)))?   // true = NATS-backed
        .with_plugin(Arc::new(TracingLogger::default()))?
        .with_plugin(Arc::new(NatsBlobstore::new(&data_nats)))?
        .with_plugin(Arc::new(NatsMessaging::new(data_nats.clone())))?
        .with_plugin(Arc::new(NatsKeyValue::new(&data_nats)))?;

    // Optionally enable HTTP
    let http_router = DynamicRouter::default();
    let http_server = Ingress::new(http_router, "0.0.0.0:8080".parse()?).await?;
    builder = builder.with_http_handler(Arc::new(http_server));

    let cluster_host = builder.build()?;

    // Start the host and run until shutdown
    let shutdown = run_cluster_host(cluster_host).await?;

    println!("Cluster host running. Press Ctrl+C to stop.");
    tokio::signal::ctrl_c().await?;

    // Graceful shutdown
    shutdown.await?;
    println!("Host stopped.");
    Ok(())
}

ClusterHostBuilder methods​

MethodDescription
with_host_group(str)Host group label for workload scheduling
with_host_name(str)Human-readable name (auto-generated if not set)
with_nats_client(Arc<Client>)NATS client for the control plane
with_plugin(Arc<dyn HostPlugin>)Register a plugin (IDs must be unique)
with_http_handler(Arc<dyn HostHandler>)Register the HTTP handler
with_host_builder(HostBuilder)Provide a pre-configured HostBuilder for advanced use
with_artifact_cleaner(frequency, max_age)Clean up stale cached artifacts on a schedule
build()Construct the ClusterHost

NATS-backed plugins​

In cluster mode, use NATS-backed plugin implementations so that workloads can access shared state across hosts:

PluginTypeDescription
ConfigDynamicConfig::new(true)NATS-backed runtime config (pass false for dev/in-memory)
LoggingTracingLogger::default()Structured logging via tracing
Key-valueNatsKeyValue::new(&nats_client)NATS-backed key-value store
BlobstoreNatsBlobstore::new(&nats_client)NATS-backed blob storage
MessagingNatsMessaging::new(nats_client)NATS pub/sub messaging

Attaching control to a host you own​

ClusterHostBuilder owns host construction: you hand it a HostBuilder, plugins, and an HTTP handler, and it builds, starts, and owns the Host. Since 2.11.0 AttachedHostControl inverts that, serving the same NATS control API for a host you built, started, and keep owning.

rust
use wash_runtime::washlet::{AttachedHostControl, HostCommandHandler, HostControlDefaults};

let control = AttachedHostControl::builder(host.clone(), nats_client)
    .with_host_group("my-cluster")
    .with_handler(Arc::new(MyPolicy))
    .attach()
    .await?;

// ... later, stop serving control while the host keeps running
control.shutdown().await?;

The builder also takes with_heartbeat_interval (zero means 15s), with_max_concurrent_starts, with_artifact_cleaner(frequency, max_age), and with_liveness. Dropping the AttachedHostControl aborts the loop.

What this makes possible is one host serving both locally-originated and operator-originated workloads, with your code arbitrating which workloads the control plane may see or touch. A HostCommandHandler intercepts every remote start, stop, and status, and can refuse it, rewrite it, or delegate to HostControlDefaults for stock behavior:

rust
#[async_trait]
impl HostCommandHandler for MyPolicy {
    async fn start(&self, defaults: &HostControlDefaults, req: WorkloadStartRequest)
        -> anyhow::Result<WorkloadStartResponse> {
        if !req.workload_id.starts_with("owned-") {
            anyhow::bail!("admission denied");
        }
        defaults.start(req).await
    }
}

Nothing in the runtime distinguishes a locally-started workload from an operator-started one, so protecting one is the handler's job: HostControlDefaults::stop stops whatever workload_id it is given. Refuse a stop or status whose id your own code owns, as the example above refuses a start. What the runtime does guarantee is that only one attachment may be active at a time — a second is refused rather than silently layered over the first, so a handler's policy cannot be bypassed by attaching again.

Note that with_host_group sets the host group label in heartbeats only; it does not add a hostgroup label to the host itself. ClusterHostBuilder has no with_handler, so command interception means moving onto this path.

A new NATS permission, for restrictive auth only

Since 2.11.0 a host proves its control subscription is live before reporting started: it subscribes, publishes a marker to runtime.host.<host_id>.__control.ready, and waits to receive its own message back. The connection must therefore be allowed to publish to that subject and must not use no_echo. Failing either, startup fails after 10 seconds with a message naming the subjects, instead of a host that starts cleanly and silently serves no commands. The chart's bundled NATS applies no subject-level permissions, so a stock install is unaffected; this matters only where you have hand-rolled restrictive NATS authorization.

How the operator communicates with washlets​

The operator sends commands to washlets by publishing to NATS subjects:

runtime.host.<host_id>.workload.start
runtime.host.<host_id>.workload.stop
runtime.host.<host_id>.workload.status
runtime.host.<host_id>.heartbeat

Washlets publish heartbeats to:

runtime.operator.heartbeat.<host_id>

Messages are JSON-encoded. You typically don't interact with these subjects directly—the operator handles scheduling and the washlet handles command processing.

Keep reading​