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
| Platform | Runtime | Guest languages |
|---|---|---|
| Cloudflare Workers | V8 isolates (JS + WASM) | JS, Rust, Go, Python |
| Fastly Compute | Lucet → Wasmtime | Rust, JS, Go, C |
| Deno Deploy | V8 + Deno WASM | JS, TS |
| Fermyon Spin | Wasmtime | Rust, Go, Python |
| wazero (this lab) | Pure Go WASM runtime | Any 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(notMustInstantiate).MustInstantiatepanics on error;Instantiatereturns 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