Yesterday I did an experiment: I took this blog — a plain Jekyll site that has been deployed over SFTP to shared hosting for years — and made it serve through a WebAssembly binary running server-side on Cloudflare Workers.
The entire “server” is about 80 lines of Rust, compiled to wasm. Gzipped, the
binary is 145 KB. It handles everything my Apache .htaccess used to do:
the canonical-host 301 redirect, six security headers on every response,
DirectoryIndex resolution, trailing-slash redirects, and the 404 page. The
images and CSS never touch the wasm at all — Cloudflare’s static assets
binding serves them straight from the CDN, free and unlimited.
I went in expecting a fun toy. I came out convinced this is how I want to deploy a whole class of services. Five reasons.
1. The artifact is absurdly small
The deployable unit is 145 KB gzipped — smaller than most header images on this blog. A Docker image shipping the same routing logic drags along a base image, a distro, and a web server: tens to hundreds of megabytes that aren’t your code. A wasm module is just compiled logic against a standard runtime interface; the runtime is the platform’s problem.
2. You test the exact artifact you ship
wrangler dev runs the same workerd runtime locally that Cloudflare runs at
the edge — same wasm binary, same bindings, up in milliseconds. My acceptance
script (twelve curl checks: header parity, byte-identical HTML, redirect
status codes) runs unchanged against localhost and the deployed URL. No image
build, no registry round-trip, no environment drift.
3. The CDN does the heavy lifting — for free
The 10 MB worker limit forces the right architecture anyway: logic in the binary, bytes on the CDN. This blog’s ~74 MB of images, CSS, and fonts are served by Cloudflare’s static assets binding — edge-cached, zero egress fees, not counted against the worker’s request quota. The wasm only wakes up for redirects, headers, and 404s.
4. The economics are embarrassing
$0 per month: 100,000 worker requests/day free, static assets unlimited, no bandwidth charges; $5/month past that. And because wasm instantiates in milliseconds, scale-to-zero has no cold-start penalty — the problem you engineer around with containers doesn’t exist here.
5. It’s real server logic, not a static-hosting hack
The worker does what Apache did via mod_rewrite and mod_headers — canonical
301s, security headers, 404s — in 80 lines of typed, testable Rust instead of
.htaccess regex. The same shape carries a real backend: routing, auth,
database and upstream calls in one small binary. And with WASI it isn’t
vendor-locked — the same module runs on wasmtime, Fastly, or Spin.
The honest caveats
It wasn’t friction-free, and I’d rather you hit these in this paragraph than at midnight:
- Toolchain sharp edges.
worker-buildsilently requires a recentworkercrate version; enabling LTO in the release profile brokewasm-bindgenwith a cryptic “externref table” error. Both fixable in minutes once diagnosed, both annoying to diagnose. - Platform defaults fight parity. Cloudflare’s default URL handling
307-redirects
.htmlURLs to extensionless ones — nice for a new site, wrong when you need byte-parity with an existing Apache setup. One config line (html_handling = "none") and a few lines of Rust fixed it. - The 10 MB limit is real. Wasm-on-Workers is for logic, not payloads. If your service is mostly moving large files, this isn’t your architecture.
Takeaway
A container ships a machine; a wasm module ships a function. For services whose job is logic rather than bulk data — redirects today, APIs tomorrow — the wasm version is orders of magnitude smaller, tests as the identical artifact you deploy, starts in milliseconds, and rides a free CDN for everything heavy.
The blog was the toy problem. Over the next weeks I want to push actual backend logic through this pipeline — a real service with state and upstream calls — and see where the model bends. If it holds, I struggle to justify a Dockerfile for small services again.
