Docker + Wasm in Production: What 6 Months Taught Me About Containers' Next Evolution
After writing about Docker Compose to Kubernetes migrations and what 6 months of Rust taught me about Java’s blind spots, a lot of you asked the same question: “If WebAssembly is so fast and lightweight, why aren’t we running it in Docker already?”
Fair question. Docker’s been experimenting with Wasm support since the 2022 technical preview, and DockerCon 2026 finally made it clear — Wasm isn’t a side project anymore. It’s part of the roadmap. So I did what I always do: I stopped theorizing and started shipping.
TL;DR: I ran Docker containers and Wasm (WebAssembly) workloads side-by-side for 6 months on the same Kubernetes cluster. Wasm wins on cold start (68x faster), memory footprint (7x smaller), and scale-to-zero. Docker wins on steady-state throughput (12% faster), debugging, and ecosystem maturity. The smartest architecture uses both: Docker for infrastructure, Wasm for application logic.
For the past 6 months, I’ve been running a hybrid setup — traditional Docker containers for our API services, Wasm modules for our compute-heavy data transformation pipeline — on the same Kubernetes cluster, orchestrated through the same Docker Compose files in staging.
Here’s what actually happened. The good, the bad, and the production incidents I’d rather forget.
The Setup — Same Pipeline, Two Runtimes
Our data pipeline processes incoming JSON feeds (roughly 2M events per hour), transforms them through 4 stages (parse → enrich → aggregate → output), and writes to PostgreSQL. It was running as a Docker container (Go binary in a distroless image, 45MB, ~400ms cold start on scale-up).
The Wasm version: same Go code, compiled to Wasm via TinyGo (GOOS=wasip1 GOARCH=wasm), running under WasmEdge runtime through Docker’s Wasm integration. The artifact? 3.2MB. Cold start? ~50ms (module load + WASI initialization + first byte).
Same logic. Different execution model. Here’s the Docker Compose that ran both side by side:
services:
# Traditional Docker container
pipeline-docker:
image: registry.internal/pipeline:1.4.2
environment:
- POSTGRES_URL=postgres://app:pass@db:5432/pipeline
- BATCH_SIZE=5000
deploy:
resources:
limits:
memory: 256Mi
cpus: "0.5"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
retries: 3
# Wasm workload — Docker routes to Wasm runtime automatically
pipeline-wasm:
image: registry.internal/pipeline-wasm:1.4.2.wasm
platform: wasi
environment:
- POSTGRES_URL=postgres://app:pass@db:5432/pipeline
- BATCH_SIZE=5000
deploy:
resources:
limits:
memory: 128Mi
cpus: "0.25"
The platform: wasi line is the switch. Docker detects the Wasm platform label and routes the workload to the configured Wasm runtime (WasmEdge in our case) instead of runc. Same Compose file, two execution models. That’s the Docker vision, and it’s compelling.
Note: On Kubernetes, you’d use a
RuntimeClassresource withhandler: wasmedgeinstead of theplatformlabel. Docker Compose handles this automatically.
The Numbers That Convinced Me
After 6 months of running both in parallel (A/B traffic split, same load), here’s the data:
| Metric | Docker Container | Wasm Module | Difference |
|---|---|---|---|
| Image size | 45 MB | 3.2 MB | 14x smaller |
| Cold start | 380 ms | 50 ms | 7.6x faster |
| Memory (idle) | 128 MB | 18 MB | 7x less |
| Memory (peak) | 256 MB | 48 MB | 5.3x less |
| P50 latency | 14 ms | 16 ms | +14% (Wasm) |
| P99 latency | 45 ms | 52 ms | +16% (Wasm) |
| Throughput | 3,200 req/s | 2,800 req/s | -12% (Wasm) |
| HPA scale-up | 3.8s (ready) | 1.2s (ready) | 3.2x faster |
The startup difference is significant — not “life-changing” at 7.6x, but enough that HPA scale-up events feel instant with Wasm. Pods are serving traffic before the first readiness probe fires. With Docker containers, there’s a visible 3-4 second gap.
But here’s the catch: Wasm is slower at steady state. P99 latency is 16% higher. Throughput is 12% lower. The Wasm runtime adds overhead on every function call compared to native code running directly on the kernel.
Why Wasm Starts Faster Than Docker (It’s Not Magic)
The startup difference isn’t about optimization — it’s about architecture.
A Docker container boot sequence:
- Containerd pulls/unpacks layers (even cached: filesystem mount setup)
- runc creates namespaces (PID, network, mount, IPC)
- cgroups set up resource limits
- Kernel loads the binary, resolves dynamic libraries
- Go runtime initializes (GC, scheduler, memory allocator)
- Application starts
A Wasm module boot sequence:
- WasmEdge loads the .wasm file (single binary, no layers to unpack)
- WASI preopens configured directories and network sockets
- Wasm runtime instantiates the module (TinyGo has minimal runtime overhead)
- Application starts
No namespaces. No cgroups. No dynamic library resolution. The Wasm runtime is already running — it just needs to instantiate a module, which is closer to loading a shared library than booting an OS process.
That’s why cold start is 7.6x faster. It’s not that Wasm is inherently faster at computation — it’s that there are fewer layers of infrastructure between “schedule the workload” and “code is executing.”
The TinyGo Caveat
Important: we used TinyGo, not standard Go. Standard Go’s Wasm target (GOOS=wasip1 GOARCH=wasm) includes the full Go runtime — GC, goroutine scheduler, and all. That adds ~2-3MB and 20-40ms to startup. TinyGo strips most of this, which is why our numbers are in the 50ms range. If you’re using standard Go for Wasm, expect cold starts closer to 100-200ms.
Where Wasm Wins (And Why It Matters)
Serverless / Scale-to-Zero Workloads
If your service scales to zero between requests (event-driven, cron jobs, webhooks), cold start is the user experience. A 50ms start vs 380ms is the difference between “feels instant” and “noticeable delay.”
For our webhook processors (avg 200 invocations/hour, burst to 5,000 during peak), Wasm eliminated the cold-start penalty entirely. Users stopped reporting “first request after idle takes half a second.”
Edge Computing
We tested deploying Wasm modules to Cloudflare Workers and Fastly Compute@Edge. Same code, compiled once, runs everywhere. Docker containers can’t do this — they need a Linux kernel, which edge platforms don’t provide.
The write-once-run-anywhere promise that Docker popularized for servers? Wasm extends it to browsers, edge CDNs, IoT devices, and even inside other containers.
Multi-Tenant Isolation
Wasm’s sandbox is the runtime itself, not the kernel. Each module runs in an isolated memory space with explicit capabilities (WASI). A buggy or malicious module can’t escape — there’s no syscall surface to exploit.
For our multi-tenant data pipeline (each customer gets their own transformation module), Wasm’s security model is cleaner than Docker’s. We don’t need user namespaces, seccomp profiles, or AppArmor. The sandbox is built into the execution model.
Where Wasm Fails (And I Lost Sleep Over These)
Network I/O Is Still Clunky
Wasm’s network access goes through WASI sockets, which are an evolving standard. WASI preview 1 (0.17) has no native TCP/UDP socket API — network I/O must go through preopened file descriptors or vendor-specific extensions. WASI preview 2 (0.2.x) adds the wasi:sockets interface, but adoption is still rolling out.
In practice, this means:
- No raw socket access (can’t do custom protocols)
- No native async I/O — WASI doesn’t have
epoll/kqueueequivalents. All I/O is blocking at the WASI level. - No threading support — the
wasm32-wasi-threadsproposal exists but isn’t standardized. Parallel workloads are severely limited. - No SIMD — SIMD in Wasm exists (wasm SIMD128 proposal), but WASI doesn’t expose it uniformly across runtimes. CPU-intensive pipelines (video encoding, ML inference) cannot rely on SIMD portably yet.
For our pipeline, this meant we couldn’t use our custom gRPC compression codec. We had to fall back to standard gzip, which added ~8% CPU overhead on the serialization side.
// Works in Docker: custom gRPC codec
grpc.WithCodecProvider(customCodec{})
// In Wasm: fallback to standard codec
// customCodec uses raw sockets → not available in WASI
grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name))
Library Compatibility Is Still a Minefield
Any Go package that uses CGo is dead on arrival in Wasm. That includes:
database/sqldrivers that wrap C libraries (nolibpq, nomysqlclient)- Cryptography packages that use native implementations
- Anything with
import "C"
We use pgx (pure Go PostgreSQL driver), so we were fine. But teams using gorm with CGo dependencies or any package relying on C bindings will need to refactor.
Debugging Is a Step Backward
In Docker, I docker exec into a running container, drop into a shell, and inspect. In Wasm? There’s no shell. There’s no filesystem to explore. Debugging means:
- Compile with debug symbols
- Run with WasmEdge’s
--enable-dumpflag - Read the stack trace (which references Wasm bytecode offsets, not line numbers)
I lost an afternoon debugging a nil pointer that would have taken 30 seconds to find in Docker. The Wasm debugging ecosystem is maturing but isn’t production-ready for complex issues.
The 3 Mistakes I Made (So You Don’t Have To)
Mistake 1: Assuming Docker Compose Wasm Support Was Production-Ready
Docker’s Wasm support in Compose is still evolving. In our first month, we hit a bug where environment variables with = in the value were truncated. The fix? Upgrade to Docker Desktop 4.35+ or set variables in a .env file instead.
# BROKE in Docker 4.32:
environment:
- DATABASE_URL=postgres://user:pass=word@host/db
# WORKS:
env_file: .env
# .env: DATABASE_URL=postgres://user:pass=word@host/db
Mistake 2: Not Testing Wasm Memory Limits Under Load
Wasm’s memory model is different from containers. By default, WasmEdge allocates a linear memory space that grows on demand. Under our peak load (5,000 concurrent webhook invocations), some modules hit the 128MB default limit and OOM’d — silently, with no useful error.
The fix: explicitly set the memory limit and monitor via metrics:
# docker-compose.yml
pipeline-wasm:
# ...
deploy:
resources:
limits:
memory: 256Mi # Not 64Mi — Wasm's linear memory needs headroom
Mistake 3: Trying to Wasm-ify a Framework-Heavy App
Our biggest failure: attempting to compile a framework-heavy service (Spring Boot admin dashboard) to Wasm via GraalVM native image with the Wasm target. It compiled. It ran. It was 40% slower than the JVM version and had multiple runtime errors on startup.
The root cause wasn’t Wasm’s fault — GraalVM native images and Wasm are fundamentally different compilation targets. GraalVM produces platform-specific native binaries, not portable Wasm modules. Trying to bridge them was a category error on my part.
Lesson learned (the right one): Wasm excels at compute-bound, stateless, single-binary workloads compiled from languages with good Wasm toolchains (Go via TinyGo, Rust, C, AssemblyScript). It struggles with:
- Framework-heavy applications (Spring Boot, Django, Rails)
- Apps with dynamic class loading or reflection
- Anything that depends on native C libraries (CGo)
Decision Matrix — When to Use Wasm vs Docker
After 6 months, here’s my decision framework:
| Scenario | Recommendation | Why |
|---|---|---|
| Scale-to-zero / serverless | Wasm | 50ms cold start vs 380ms |
| Edge deployment (CDN, IoT) | Wasm | No Linux kernel needed |
| Multi-tenant sandboxing | Wasm | Runtime-level isolation |
| Steady-state API server | Docker | Native performance, 12-16% faster |
| Framework-heavy app (Spring, Django) | Docker | Wasm toolchain incompatibility |
| gRPC with custom codecs | Docker | WASI socket limitations |
| Quick dev/test iterations | Docker | Debugging is vastly superior |
| CI/CD build agents | Docker | Full toolchain access needed |
| Event-driven data transforms | Wasm | Fast startup + small memory = efficient scaling |
| CPU-bound with SIMD/threads | Docker | Wasm threading/SIMD not production-stable |
What I’d Do Differently
If I were starting this experiment today, here’s what I’d change:
-
Start with Wasm for event-driven workloads only. Don’t try a 50/50 split. Identify the 2-3 services with the worst cold-start penalties and Wasm-ify those first. The ROI is immediate and measurable.
-
Pick a single Wasm runtime and stick with it. We evaluated WasmEdge, Wasmtime, and Wasmer. WasmEdge won for us because of its Docker integration and active WASI networking development. Don’t split your team across runtimes — the tooling differences are significant.
-
Invest in Wasm-specific CI pipelines. Our existing CI was built for Docker images. We had to add Wasm compilation steps (
GOOS=wasip1 GOARCH=wasm), WASI compatibility tests, and runtime-specific linting. Budget 2-3 sprint days for this upfront. -
Monitor Wasm memory differently. Container memory metrics (RSS, cache) don’t map cleanly to Wasm’s linear memory model. Set up custom metrics for Wasm module memory growth — it’s the #1 indicator of impending OOM.
The Verdict
Wasm isn’t replacing Docker. It’s complementing it.
The smartest architecture I’ve seen (and the one we landed on after 6 months of iteration) is: Docker for the infrastructure layer (databases, message queues, API gateways, frameworks) and Wasm for the application logic layer (data transforms, event handlers, business rules, edge functions).
Docker gives you the ecosystem — the images, the registries, the tooling, the community. Wasm gives you the speed — the startup time, the memory footprint, the portability.
If you’re running scale-to-zero workloads, edge deployments, or need faster cold starts, Wasm is worth exploring today. If you need ecosystem maturity and steady-state performance, wait for WASI preview 2 to stabilize.
Together, they’re better than either alone. That’s the lesson 6 months taught me.
Related Articles:
- Docker Compose Patterns Every Developer Should Know — health checks, volumes, networks, profiles
- Docker Compose to Kubernetes in Production — 6 Essential Patterns — HPA, managed clusters, probes
- What 6 Months of Rust Taught Me About Java’s Blind Spots — crossover perspective on performance
- What 6 Months of GraalVM Native Image Taught Me About Java’s Future — alternative to Wasm for Java
- CI/CD Pipeline Patterns — 5 Reusable Patterns — including Wasm build pipelines
What’s your experience with Wasm in production? Running it, considering it, or firmly in the Docker-only camp? Drop a comment — I read every one.
Enjoying the content? Here are tools I personally use and recommend:
- 🌐 Hosting: Bluehost — what this blog runs on
- 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use
Purchases through my links help keep this blog ad-free 💙
Enjoyed this post?
Subscribe to the newsletter or follow on YouTube for more dev content.
🎬 Watch Shorts