Skip to content

Run Rust WebAssembly on Cloudflare edge and wasmCloud

This guide explains how this template structures pure-Rust WebAssembly components so the exact same state machine, SHA-256 fingerprint diff-gating, distributed lease locking, and HMAC-SHA256 authentication run natively on both Cloudflare Workers (wasm32-unknown-unknown + worker::kv::KvStore) and wasmCloud on Kubernetes (wasm32-wasip2 + wasmcloud:keyvalue / wasi:keyvalue).

Dual-runtime architecture overview

Dimension Cloudflare Edge + KV wasmCloud on Kubernetes
Compilation target wasm32-unknown-unknown (packaged by worker-build) wasm32-wasip2 (WebAssembly Component Model)
Rust SDK and entrypoint workers-rs (worker crate, #[event(fetch)] and #[event(scheduled)]) wasmcloud-component (wasi:http/incoming-handler)
Direct KV binding worker::kv::KvStore via env.kv("STATE_KV")? wasi:keyvalue/store (wasmcloud:keyvalue) bucket
Storage trait implementation impl KvStoreBackend for worker::kv::KvStore impl<B: WasiKeyValueBucket> KvStoreBackend for WasmCloudKvAdapter<B>
Underlying backing store Globally replicated Cloudflare Workers KV Kubernetes-hosted NATS JetStream, Redis, or Valkey capability provider
flowchart TD
    Domain["Shared Rust Domain & State Engine (StateBundle, SHA-256 Fingerprint, Leases, HMAC Auth)"]
    Trait["KvStoreBackend Trait (cloudflare/src/state.rs)"]
    Domain --> Trait
    Trait --> CF["Cloudflare Edge (wasm32-unknown-unknown)<br/>worker::kv::KvStore -> STATE_KV"]
    Trait --> WC["wasmCloud on Kubernetes (wasm32-wasip2)<br/>WasmCloudKvAdapter -> wasi:keyvalue/store"]
    Trait --> Mem["Native Test Harness (cargo test)<br/>InMemoryKvStore"]

Part 1 for using Wasm at Cloudflare edge with Workers KV

On Cloudflare's edge network, you write WebAssembly components in Rust by using workers-rs (worker = "0.8.7" in cloudflare/Cargo.toml), compile them to wasm32-unknown-unknown with worker-build --release, and bind Cloudflare Workers KV directly through worker::kv::KvStore.

Bind worker::kv::KvStore directly in workers-rs

In cloudflare/src/lib.rs, both HTTP handlers (#[event(fetch)]) and cron triggers (#[event(scheduled)]) obtain the worker::kv::KvStore binding directly from the Worker Env and pass it to the state engine:

use worker::*;
use crate::state::{load_state, save_state_if_changed, WRITER_SLOT_EDGE};

#[event(fetch)]
pub async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    // Bind Cloudflare Workers KV directly through worker::kv::KvStore
    let kv: worker::kv::KvStore = env.kv("STATE_KV")?;
    let now = crate::current_time_epoch();

    let (bundle, cache_source) = load_state(&kv, now)
        .await
        .map_err(|e| Error::RustError(e.to_string()))?;

    Response::from_json(&serde_json::json!({
        "cache_source": cache_source,
        "bundle": bundle,
    }))
}

Direct KvStoreBackend implementation for worker::kv::KvStore

cloudflare/src/state.rs implements KvStoreBackend directly on worker::kv::KvStore:

#[async_trait::async_trait(?Send)]
impl KvStoreBackend for worker::kv::KvStore {
    async fn get(&self, key: &str) -> Result<Option<String>, StateError> {
        self.get(key)
            .text()
            .await
            .map_err(|e| StateError::Kv(e.to_string()))
    }

    async fn put(&self, key: &str, value: &str) -> Result<(), StateError> {
        self.put(key, value)
            .map_err(|e| StateError::Kv(e.to_string()))?
            .execute()
            .await
            .map_err(|e| StateError::Kv(e.to_string()))
    }

    async fn delete(&self, key: &str) -> Result<(), StateError> {
        self.delete(key)
            .await
            .map_err(|e| StateError::Kv(e.to_string()))
    }
}

Because worker::kv::KvStore is single-threaded inside a Wasm isolate, #[async_trait::async_trait(?Send)] avoids requiring Send bounds while allowing zero-cost async dispatch on wasm32-unknown-unknown.

Part 2 for running the same Rust worker on wasmCloud and Kubernetes

When deploying to Kubernetes with wasmCloud, components target wasm32-wasip2 and access key-value state through the standardized wasi:keyvalue/store (wasmcloud:keyvalue) interface rather than JavaScript host bindings.

Bridge wasi:keyvalue with WasiKeyValueBucket and WasmCloudKvAdapter

cloudflare/src/state.rs provides WasiKeyValueBucket and WasmCloudKvAdapter<B>, which adapt any byte-oriented wasi:keyvalue/store bucket into a KvStoreBackend:

pub trait WasiKeyValueBucket {
    fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, String>;
    fn set_bytes(&self, key: &str, value: &[u8]) -> Result<(), String>;
    fn delete_key(&self, key: &str) -> Result<(), String>;
}

In a wasmCloud component crate (compiled to wasm32-wasip2), implement WasiKeyValueBucket for wasi::keyvalue::store::Bucket and pass WasmCloudKvAdapter::new(bucket) directly into load_state, save_state_if_changed, acquire_lease, and run_reconciliation_tick:

use cf_worker_kv_template::state::{
    load_state, save_state_if_changed, WasiKeyValueBucket, WasmCloudKvAdapter, WRITER_SLOT_EDGE,
};

pub struct WasiBucketHandle {
    bucket: wasmcloud_component::wasi::keyvalue::store::Bucket,
}

impl WasiKeyValueBucket for WasiBucketHandle {
    fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
        self.bucket.get(key).map_err(|e| format!("{e:?}"))
    }

    fn set_bytes(&self, key: &str, value: &[u8]) -> Result<(), String> {
        self.bucket.set(key, value).map_err(|e| format!("{e:?}"))
    }

    fn delete_key(&self, key: &str) -> Result<(), String> {
        self.bucket.delete(key).map_err(|e| format!("{e:?}"))
    }
}

Deploy on Kubernetes with a wadm.yaml manifest

On Kubernetes running the wasmCloud operator, wire the Rust WebAssembly component to an HTTP server provider and a wasi:keyvalue capability provider (backed by NATS JetStream or Redis) using a declarative wadm.yaml manifest:

apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: rust-kv-edge-worker
  annotations:
    version: v0.1.0
    description: "Portable Rust Wasm worker using wasi:keyvalue on Kubernetes"
spec:
  components:
    - name: rust-kv-worker
      type: component
      properties:
        image: ghcr.io/swack-tools/webassemply-template:v0.1.0
      traits:
        - type: spreadscaler
          properties:
            instances: 4
        - type: link
          properties:
            target: kv-jetstream
            namespace: wasi
            package: keyvalue
            interfaces: [store]
            target_config:
              - name: state-bucket
                properties:
                  bucket: STATE_KV

    - name: http-server
      type: capability
      properties:
        image: ghcr.io/wasmcloud/http-server:0.23.0
      traits:
        - type: link
          properties:
            target: rust-kv-worker
            namespace: wasi
            package: http
            interfaces: [incoming-handler]
            source_config:
              - name: default-http
                properties:
                  address: 0.0.0.0:8080

    - name: kv-jetstream
      type: capability
      properties:
        image: ghcr.io/wasmcloud/keyvalue-nats-kv:0.4.0

Why the consolidated document pattern benefits both runtimes

Preserving the single consolidated document (app:state), SHA-256 fingerprint diff-gating, and thread_local! isolate caching improves performance on both platforms:

  • On Cloudflare Workers: keeps daily KV read and write operations well within Free Tier limits and serves warm isolate reads in under 0.1 milliseconds.
  • On wasmCloud + Kubernetes: minimizes NATS JetStream or Redis round trips across pod boundaries, ensures atomic single-key state updates (app:state), and prevents redundant raft log growth on no-op reconciliation ticks.