Docker + Wasm in Production: What 6 Months Taught Me About Containers' Next Evolution

📅 May 30, 2026
Docker + Wasm in Production: What 6 Months Taught Me About Containers' Next Evolution
👁 ... views

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 RuntimeClass resource with handler: wasmedge instead of the platform label. 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:

MetricDocker ContainerWasm ModuleDifference
Image size45 MB3.2 MB14x smaller
Cold start380 ms50 ms7.6x faster
Memory (idle)128 MB18 MB7x less
Memory (peak)256 MB48 MB5.3x less
P50 latency14 ms16 ms+14% (Wasm)
P99 latency45 ms52 ms+16% (Wasm)
Throughput3,200 req/s2,800 req/s-12% (Wasm)
HPA scale-up3.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:

  1. Containerd pulls/unpacks layers (even cached: filesystem mount setup)
  2. runc creates namespaces (PID, network, mount, IPC)
  3. cgroups set up resource limits
  4. Kernel loads the binary, resolves dynamic libraries
  5. Go runtime initializes (GC, scheduler, memory allocator)
  6. Application starts

A Wasm module boot sequence:

  1. WasmEdge loads the .wasm file (single binary, no layers to unpack)
  2. WASI preopens configured directories and network sockets
  3. Wasm runtime instantiates the module (TinyGo has minimal runtime overhead)
  4. 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/kqueue equivalents. All I/O is blocking at the WASI level.
  • No threading support — the wasm32-wasi-threads proposal 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/sql drivers that wrap C libraries (no libpq, no mysqlclient)
  • 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:

  1. Compile with debug symbols
  2. Run with WasmEdge’s --enable-dump flag
  3. 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:

ScenarioRecommendationWhy
Scale-to-zero / serverlessWasm50ms cold start vs 380ms
Edge deployment (CDN, IoT)WasmNo Linux kernel needed
Multi-tenant sandboxingWasmRuntime-level isolation
Steady-state API serverDockerNative performance, 12-16% faster
Framework-heavy app (Spring, Django)DockerWasm toolchain incompatibility
gRPC with custom codecsDockerWASI socket limitations
Quick dev/test iterationsDockerDebugging is vastly superior
CI/CD build agentsDockerFull toolchain access needed
Event-driven data transformsWasmFast startup + small memory = efficient scaling
CPU-bound with SIMD/threadsDockerWasm threading/SIMD not production-stable

What I’d Do Differently

If I were starting this experiment today, here’s what I’d change:

  1. 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.

  2. 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.

  3. 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.

  4. 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:


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