Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lab 17 · Edge Compute via WebAssembly

Run it: make lab-17
Source: labs/lab-17-edge-compute/main.go


The Problem

Every CDN feature we’ve built so far is fixed at deployment time: the routing logic, the cache key normalization, the compression settings. What if you want application-specific logic at the edge that changes independently of the CDN infrastructure?

Use cases:

  • Custom request routing logic (A/B test, feature flag)
  • Bot and device detection
  • Request authentication and rate limiting
  • Header manipulation (add, remove, rewrite)
  • Edge-rendered personalisation fragments
  • URL rewriting and canonical redirects

Traditionally this required deploying custom CDN software (Nginx modules, Varnish VMODs) or Lua/JS scripts (Nginx Lua, CloudFront Lambda@Edge). WebAssembly (WASM) provides a more universal and safer alternative.


WebAssembly at the Edge

WebAssembly is a binary instruction format designed for safe, fast execution in sandboxed environments. Key properties for edge compute:

  • Sandboxed: WASM modules cannot access the filesystem, network, or system calls directly. All I/O is mediated by the host.
  • Language-agnostic: Compile Go, Rust, C, AssemblyScript, or any WASM target to the same binary format.
  • Near-native speed: WASM runtime compiles to machine code; typical overhead is 5–10% vs. native.
  • Instant startup: WASM modules start in ~50 µs. Lambda/container cold starts are 100 ms–10 s.

Production edge compute platforms using WASM

PlatformRuntimeGuest languages
Cloudflare WorkersV8 isolates (JS + WASM)JS, Rust, Go, Python
Fastly ComputeLucet → WasmtimeRust, JS, Go, C
Deno DeployV8 + Deno WASMJS, TS
Fermyon SpinWasmtimeRust, Go, Python
wazero (this lab)Pure Go WASM runtimeAny WASI target

wazero: Pure Go WASM Host

tetratelabs/wazero is a zero-dependency, pure Go WASM runtime that implements WASI (WebAssembly System Interface). It runs WASM modules in-process:

import (
    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

// Create a runtime
ctx := context.Background()
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)

// Instantiate the WASI environment (stdin/stdout/stderr for the module)
wasi_snapshot_preview1.MustInstantiate(ctx, r)

// Load and compile the WASM binary
wasmBinary, _ := os.ReadFile("detector.wasm")
mod, _ := r.InstantiateModuleFromBinary(ctx, wasmBinary)

// Call a function exported by the WASM module
fn := mod.ExportedFunction("detect_bot")
result, _ := fn.Call(ctx, /* args... */)

API note: The lab uses wasi_snapshot_preview1.Instantiate (not MustInstantiate). MustInstantiate panics on error; Instantiate returns an error, which is more appropriate in a production handler.


The Guest WASM Module

The WASM “guest” is compiled from Go with GOOS=wasip1 GOARCH=wasm:

//go:build wasip1

package main

import (
    "strings"
    "unsafe"
)

// DetectBot checks if the User-Agent is a known bot
//
//export detect_bot
func DetectBot(uaPtr uint32, uaLen uint32) uint32 {
    ua := ptrToString(uaPtr, uaLen)
    if isBotUA(ua) { return 1 }
    return 0
}

func isBotUA(ua string) bool {
    lower := strings.ToLower(ua)
    bots := []string{"googlebot", "bingbot", "slurp", "duckduckbot",
                      "baiduspider", "yandexbot", "sogou", "facebot",
                      "ia_archiver", "curl/", "python-requests", "go-http"}
    for _, bot := range bots {
        if strings.Contains(lower, bot) { return true }
    }
    return false
}

// ptrToString converts a WASM memory pointer+length to a Go string
func ptrToString(ptr, length uint32) string {
    var buf []byte
    s := (*[1 << 30]byte)(unsafe.Pointer(uintptr(ptr)))
    buf = s[:length:length]
    return string(buf)
}

func main() {} // required for WASI

Build:

GOOS=wasip1 GOARCH=wasm go build -o detector.wasm ./guest/

Host-Guest Memory Communication

WASM has a flat 32-bit address space shared between host and guest. To pass a string from Go host to WASM guest:

// 1. Call the guest's allocate function to get a memory pointer
allocate := mod.ExportedFunction("allocate")
ptr, _ := allocate.Call(ctx, uint64(len(ua)))

// 2. Write the string into WASM memory
mod.Memory().Write(uint32(ptr[0]), []byte(ua))

// 3. Call the WASM function with the pointer and length
fn := mod.ExportedFunction("detect_bot")
result, _ := fn.Call(ctx, ptr[0], uint64(len(ua)))

This shared-memory model is efficient but requires careful memory management. The lab uses a simple approach (allocate once per request); production implementations use memory pools or arena allocators.


Fail-Open vs. Fail-Closed

When the WASM module errors (invalid input, out-of-memory, assertion failure), the edge has two choices:

Fail-open: serve the request normally, log the WASM error:

if err != nil {
    log.Warn("WASM error, serving anyway", "err", err)
    next.ServeHTTP(w, r)  // continue without bot detection
    return
}

Fail-closed: reject the request on WASM error:

if err != nil {
    http.Error(w, "Internal Error", 503)
    return
}

The lab uses fail-open for bot detection — if the WASM module crashes, it’s better to serve the user (potentially a bot, but probably a real user) than to block everyone.

Fail-closed is appropriate for: authentication checks, fraud detection, rate limiting where the risk of missing a check exceeds the risk of false rejection.


Cloudflare Workers Architecture

Cloudflare Workers run JavaScript (or WASM via JS) in V8 isolates — the same V8 engine used by Chrome/Node.js, but isolated per-worker:

V8 Isolate (< 128MB RAM, 10ms CPU):
  → One isolate per worker code deployment
  → Thousands of concurrent isolates per PoP
  → Cold start: ~5 ms (pre-warmed isolates: 0 ms)
  → Network: fetch() API proxied through Cloudflare infrastructure

Workers are intentionally limited to prevent abuse: no persistent state, no direct filesystem access, no raw network sockets. State must go through Workers KV (eventually consistent store), D1 (SQLite), or Durable Objects.

The WASM approach in this lab is more similar to Fastly Compute, which gives WASM modules more direct access to request/response objects.


Try It

make lab-17

# Normal request — WASM module processes the User-Agent
curl http://localhost:8080/article/1 \
  -H "User-Agent: Mozilla/5.0 (compatible; Chrome)" -v

# Bot request — should be identified and optionally blocked/tagged
curl http://localhost:8080/article/1 \
  -H "User-Agent: Googlebot/2.1" -v

# Curl (also a bot)
curl http://localhost:8080/article/1 -v
# Check X-Bot-Detected header in response

# Python-requests (bot)
curl http://localhost:8080/article/1 \
  -H "User-Agent: python-requests/2.28.0" -v